From e88e35f228b59a114bd323f353b61107857d4331 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 25 Mar 2019 10:30:44 -0700 Subject: [PATCH] feat(context): allow @inject.setter to accept binding templates --- docs/site/Decorators_inject.md | 23 +++++ .../providers/authentication.provider.unit.ts | 13 ++- .../class-level-bindings.acceptance.ts | 98 +++++++++++++------ .../src/__tests__/unit/binding.unit.ts | 13 +++ .../src/__tests__/unit/context.unit.ts | 20 ++++ packages/context/src/binding.ts | 12 ++- packages/context/src/context.ts | 20 +++- packages/context/src/inject.ts | 43 ++++++-- 8 files changed, 194 insertions(+), 48 deletions(-) diff --git a/docs/site/Decorators_inject.md b/docs/site/Decorators_inject.md index e87131f13306..26fe52851716 100644 --- a/docs/site/Decorators_inject.md +++ b/docs/site/Decorators_inject.md @@ -143,6 +143,29 @@ export class HelloController { } ``` +The `setter` function injected has the following signature: + +```ts +/** + * Set the binding with one or more `BindingTemplate` functions or values + * @param templateFnsOrValues + */ +(...templateFnsOrValues: (T | BindingTemplate)[]): Binding; +``` + +It takes either a const value or `BindingTemplate` functions to create (if not +existent) or update the binding. For example: + +```ts +const binding = this.greetingSetter('Greetings!'); +``` + +or + +```ts +const binding = this.greetingSetter(b => b.toDynamicValue(() => 'Greetings!')); +``` + ### @inject.tag `@inject.tag` injects an array of values by a pattern or regexp to match binding diff --git a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts index 26b116420691..8978d1065c2e 100644 --- a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts +++ b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts @@ -3,13 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {expect} from '@loopback/testlab'; -import {Context, instantiateClass} from '@loopback/context'; +import {Binding, Context, instantiateClass} from '@loopback/context'; import {Request} from '@loopback/rest'; -import {AuthenticateFn, UserProfile, AuthenticationBindings} from '../../..'; -import {MockStrategy} from '../fixtures/mock-strategy'; +import {expect} from '@loopback/testlab'; import {Strategy} from 'passport'; +import {AuthenticateFn, AuthenticationBindings, UserProfile} from '../../..'; import {AuthenticateActionProvider} from '../../../providers'; +import {MockStrategy} from '../fixtures/mock-strategy'; describe('AuthenticateActionProvider', () => { describe('constructor()', () => { @@ -110,7 +110,10 @@ describe('AuthenticateActionProvider', () => { strategy.setMockUser(mockUser); provider = new AuthenticateActionProvider( () => Promise.resolve(strategy), - u => (currentUser = u), + (...u) => { + currentUser = u[0] as UserProfile; + return new Binding('authentication.currentUser').to(currentUser); + }, ); currentUser = undefined; } diff --git a/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts b/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts index 046c83cd69dd..48fd99eebe70 100644 --- a/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts +++ b/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts @@ -169,6 +169,25 @@ describe('Context bindings - Injecting dependencies of classes', () => { expect(await store.getter()).to.equal('456'); }); + it('creates getter from a value', () => { + const getter = Getter.fromValue('data'); + expect(getter).to.be.a.Function(); + return expect(getter()).to.be.fulfilledWith('data'); + }); + + it('reports an error if @inject.getter has a non-function target', async () => { + ctx.bind('key').to('value'); + + class Store { + constructor(@inject.getter('key') public getter: string) {} + } + + ctx.bind(STORE_KEY).toClass(Store); + expect(() => ctx.getSync(STORE_KEY)).to.throw( + 'The type of Store.constructor[0] (String) is not a Getter function', + ); + }); + describe('in SINGLETON scope', () => { it('throws if a getter cannot be resolved by the owning context', async () => { class Store { @@ -317,49 +336,66 @@ describe('Context bindings - Injecting dependencies of classes', () => { } }); - it('injects a setter function', async () => { + describe('@inject.setter', () => { class Store { constructor(@inject.setter(HASH_KEY) public setter: Setter) {} } - ctx.bind(STORE_KEY).toClass(Store); - const store = ctx.getSync(STORE_KEY); + it('injects a setter function', async () => { + ctx.bind(STORE_KEY).toClass(Store); + const store = ctx.getSync(STORE_KEY); - expect(store.setter).to.be.Function(); - store.setter('a-value'); - expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); - }); + expect(store.setter).to.be.Function(); + store.setter('a-value'); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + }); - it('creates getter from a value', () => { - const getter = Getter.fromValue('data'); - expect(getter).to.be.a.Function(); - return expect(getter()).to.be.fulfilledWith('data'); - }); + it('injects a setter function that returns Binding', async () => { + ctx.bind(STORE_KEY).toClass(Store); + const store = ctx.getSync(STORE_KEY); - it('reports an error if @inject.getter has a non-function target', async () => { - ctx.bind('key').to('value'); + expect(store.setter).to.be.Function(); + store.setter().to('a-value'); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + }); - class Store { - constructor(@inject.getter('key') public getter: string) {} - } + it('injects a setter function to apply templates', async () => { + ctx.bind(STORE_KEY).toClass(Store); + const store = ctx.getSync(STORE_KEY); - ctx.bind(STORE_KEY).toClass(Store); - expect(() => ctx.getSync(STORE_KEY)).to.throw( - 'The type of Store.constructor[0] (String) is not a Getter function', - ); - }); + expect(store.setter).to.be.Function(); + store.setter(b => b.to('a-value').tag('a-tag')); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + expect(ctx.getBinding(HASH_KEY).tagNames).to.containEql('a-tag'); + }); - it('reports an error if @inject.setter has a non-function target', async () => { - ctx.bind('key').to('value'); + it('injects a setter function that uses an existing binding', async () => { + // Create a binding for hash key + ctx + .bind(HASH_KEY) + .to('123') + .tag('hash'); + ctx.bind(STORE_KEY).toClass(Store); + const store = ctx.getSync(STORE_KEY); + // Change the hash value + store.setter('a-value'); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + // The tag is kept + expect(ctx.getBinding(HASH_KEY).tagNames).to.containEql('hash'); + }); - class Store { - constructor(@inject.setter('key') public setter: object) {} - } + it('reports an error if @inject.setter has a non-function target', async () => { + class StoreWithWrongSetterType { + constructor(@inject.setter(HASH_KEY) public setter: object) {} + } - ctx.bind(STORE_KEY).toClass(Store); - expect(() => ctx.getSync(STORE_KEY)).to.throw( - 'The type of Store.constructor[0] (Object) is not a Setter function', - ); + ctx.bind('key').to('value'); + + ctx.bind(STORE_KEY).toClass(StoreWithWrongSetterType); + expect(() => ctx.getSync(STORE_KEY)).to.throw( + 'The type of StoreWithWrongSetterType.constructor[0] (Object) is not a Setter function', + ); + }); }); it('injects a nested property', async () => { diff --git a/packages/context/src/__tests__/unit/binding.unit.ts b/packages/context/src/__tests__/unit/binding.unit.ts index 2094da9295ef..5dfc4a693a00 100644 --- a/packages/context/src/__tests__/unit/binding.unit.ts +++ b/packages/context/src/__tests__/unit/binding.unit.ts @@ -200,6 +200,19 @@ describe('Binding', () => { expect(binding.tagNames).to.eql(['myTag']); }); + it('applies multiple template functions', async () => { + binding.apply( + b => { + b.inScope(BindingScope.SINGLETON); + }, + b => { + b.tag('myTag'); + }, + ); + expect(binding.scope).to.eql(BindingScope.SINGLETON); + expect(binding.tagNames).to.eql(['myTag']); + }); + it('sets up a placeholder value', async () => { const toBeBound = (b: Binding) => { b.toDynamicValue(() => { diff --git a/packages/context/src/__tests__/unit/context.unit.ts b/packages/context/src/__tests__/unit/context.unit.ts index f035fdaa6ed0..dd9203810ac0 100644 --- a/packages/context/src/__tests__/unit/context.unit.ts +++ b/packages/context/src/__tests__/unit/context.unit.ts @@ -342,6 +342,26 @@ describe('Context', () => { }); }); + describe('findOrCreateBinding', () => { + it('returns the binding object registered under the given key', () => { + const expected = ctx.bind('foo'); + const actual: Binding = ctx.findOrCreateBinding('foo'); + expect(actual).to.be.exactly(expected); + }); + + it('creates a new binding if not found', () => { + const binding = ctx.findOrCreateBinding('a-new-key'); + expect(binding.key).to.eql('a-new-key'); + }); + + it('rejects a key containing property separator', () => { + const key = 'a' + BindingKey.PROPERTY_SEPARATOR + 'b'; + expect(() => ctx.findOrCreateBinding(key)).to.throw( + /Binding key .* cannot contain/, + ); + }); + }); + describe('getSync', () => { it('returns the value immediately when the binding is sync', () => { ctx.bind('foo').to('bar'); diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index 0e9c7cd9fb43..26c956769e3d 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -506,8 +506,8 @@ export class Binding { } /** - * Apply a template function to set up the binding with scope, tags, and - * other attributes as a group. + * Apply one or more template functions to set up the binding with scope, + * tags, and other attributes as a group. * * For example, * ```ts @@ -517,10 +517,12 @@ export class Binding { * const serverBinding = new Binding('servers.RestServer1'); * serverBinding.apply(serverTemplate); * ``` - * @param templateFn A function to configure the binding + * @param templateFns One or more functions to configure the binding */ - apply(templateFn: BindingTemplate): this { - templateFn(this); + apply(...templateFns: BindingTemplate[]): this { + for (const fn of templateFns) { + fn(this); + } return this; } diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index dfc3f3e31556..de1412bf3842 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -38,7 +38,11 @@ if (!Symbol.asyncIterator) { // tslint:disable-next-line:no-any (Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator'); } -// This import must happen after the polyfill +/** + * This import must happen after the polyfill. + * + * WARNING: VSCode organize import may change the order of this import + */ import {iterator, multiple} from 'p-event'; const debug = debugModule('loopback:context'); @@ -746,6 +750,20 @@ export class Context extends EventEmitter { ); } + /** + * Find or create a binding for the given key + * @param key Binding address + */ + findOrCreateBinding(key: BindingAddress) { + let binding: Binding; + if (this.isBound(key)) { + binding = this.getBinding(key); + } else { + binding = this.bind(key); + } + return binding; + } + /** * Get the value bound to the given key. * diff --git a/packages/context/src/inject.ts b/packages/context/src/inject.ts index 15638e33eff5..3d2094b46141 100644 --- a/packages/context/src/inject.ts +++ b/packages/context/src/inject.ts @@ -12,7 +12,7 @@ import { ParameterDecoratorFactory, PropertyDecoratorFactory, } from '@loopback/metadata'; -import {BindingTag} from './binding'; +import {Binding, BindingTag, BindingTemplate} from './binding'; import { BindingFilter, BindingSelector, @@ -188,7 +188,9 @@ export function inject( /** * The function injected by `@inject.getter(bindingSelector)`. */ -export type Getter = () => Promise; +export interface Getter { + (): Promise; +} export namespace Getter { /** @@ -201,9 +203,28 @@ export namespace Getter { } /** - * The function injected by `@inject.setter(key)`. + * The function injected by `@inject.setter(bindingKey)`. */ -export type Setter = (value: T) => void; +export interface Setter { + /** + * Set the binding with one or more `BindingTemplate` functions or values. + * The usages are: + * + * ```ts + * setterFn('my-value'); + * setterFn(binding => binding.toClass(MyClass).tag('my-tag')); + * setterFn().toClass(MyClass); + * ``` + * @param templateFnsOrValues Binding template functions or values. Please + * note a parameter with function as the value will be treated as a template + * function. To set a function as constant value, you need to wrap it inside + * a template function, such as: + * ```ts + * setterFn(binding => binding.to(aFunction)) + * ``` + */ + (...templateFnsOrValues: (T | BindingTemplate)[]): Binding; +} export namespace inject { /** @@ -358,8 +379,18 @@ function resolveAsSetter(ctx: Context, injection: Injection) { ); } // No resolution session should be propagated into the setter - return function setter(value: unknown) { - ctx.bind(bindingSelector).to(value); + return function setter( + ...templateFnsOrValues: (BindingTemplate | unknown)[] + ) { + const binding: Binding = ctx.findOrCreateBinding(bindingSelector); + for (const templateFnOrValue of templateFnsOrValues) { + if (typeof templateFnOrValue === 'function') { + binding.apply(templateFnOrValue as BindingTemplate); + } else { + binding.to(templateFnOrValue); + } + } + return binding; }; }