diff --git a/docs/site/Decorators_inject.md b/docs/site/Decorators_inject.md index 30881db3b150..1e67fcfb77e7 100644 --- a/docs/site/Decorators_inject.md +++ b/docs/site/Decorators_inject.md @@ -116,7 +116,7 @@ from bindings that match a filter function. ```ts class MyControllerWithGetter { - @inject.getter(bindingTagFilter('prime')) + @inject.getter(filterByTag('prime')) getter: Getter; } ``` @@ -179,7 +179,7 @@ import {DataSource} from '@loopback/repository'; export class DataSourceTracker { constructor( - @inject.view(bindingTagFilter('datasource')) + @inject.view(filterByTag('datasource')) private dataSources: ContextView, ) {} @@ -190,9 +190,9 @@ export class DataSourceTracker { } ``` -In the example above, `bindingTagFilter` is a helper function that creates a -filter function that matches a given tag. You can define your own filter -functions, such as: +In the example above, `filterByTag` is a helper function that creates a filter +function that matches a given tag. You can define your own filter functions, +such as: ```ts export class DataSourceTracker { diff --git a/packages/context/src/binding-filter.ts b/packages/context/src/binding-filter.ts index 3ced61d5b977..94751fa446a3 100644 --- a/packages/context/src/binding-filter.ts +++ b/packages/context/src/binding-filter.ts @@ -15,18 +15,17 @@ export type BindingFilter = ( /** * Create a binding filter for the tag pattern - * @param tagPattern + * @param tagPattern Binding tag name, regexp, or object */ -export function bindingTagFilter(tagPattern: BindingTag | RegExp) { - let bindingFilter: BindingFilter; +export function filterByTag(tagPattern: BindingTag | RegExp): BindingFilter { if (typeof tagPattern === 'string' || tagPattern instanceof RegExp) { const regexp = typeof tagPattern === 'string' ? wildcardToRegExp(tagPattern) : tagPattern; - bindingFilter = b => Array.from(b.tagNames).some(t => regexp!.test(t)); + return b => Array.from(b.tagNames).some(t => regexp!.test(t)); } else { - bindingFilter = b => { + return b => { for (const t in tagPattern) { // One tag name/value does not match if (b.tagMap[t] !== tagPattern[t]) return false; @@ -35,22 +34,24 @@ export function bindingTagFilter(tagPattern: BindingTag | RegExp) { return true; }; } - return bindingFilter; } /** * Create a binding filter from key pattern - * @param keyPattern Binding key, wildcard, or regexp + * @param keyPattern Binding key/wildcard, regexp, or a filter function */ -export function bindingKeyFilter(keyPattern?: string | RegExp) { - let filter: BindingFilter = binding => true; +export function filterByKey( + keyPattern?: string | RegExp | BindingFilter, +): BindingFilter { if (typeof keyPattern === 'string') { const regex = wildcardToRegExp(keyPattern); - filter = binding => regex.test(binding.key); + return binding => regex.test(binding.key); } else if (keyPattern instanceof RegExp) { - filter = binding => keyPattern.test(binding.key); + return binding => keyPattern.test(binding.key); + } else if (typeof keyPattern === 'function') { + return keyPattern; } - return filter; + return () => true; } /** diff --git a/packages/context/src/binding-inspector.ts b/packages/context/src/binding-inspector.ts index cddcc7d6ab2a..0822531dadbe 100644 --- a/packages/context/src/binding-inspector.ts +++ b/packages/context/src/binding-inspector.ts @@ -174,7 +174,7 @@ export type BindingFromClassOptions = { /** * Binding key */ - key?: BindingAddress; + key?: BindingAddress; /** * Artifact type, such as `server`, `controller`, `repository` or `service` */ diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index d9ce50ad06b8..12d03180a28b 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -7,11 +7,7 @@ import * as debugModule from 'debug'; import {v1 as uuidv1} from 'uuid'; import {ValueOrPromise} from '.'; import {Binding, BindingTag} from './binding'; -import { - BindingFilter, - bindingKeyFilter, - bindingTagFilter, -} from './binding-filter'; +import {BindingFilter, filterByKey, filterByTag} from './binding-filter'; import {BindingAddress, BindingKey} from './binding-key'; import { ContextEventListener, @@ -130,7 +126,7 @@ export class Context { * @param key Binding key * @returns true if the binding key is found and removed from this context */ - unbind(key: BindingAddress): boolean { + unbind(key: BindingAddress): boolean { key = BindingKey.validate(key); const binding = this.registry.get(key); if (binding == null) return false; @@ -218,7 +214,7 @@ export class Context { * delegating to the parent context * @param key Binding key */ - contains(key: BindingAddress): boolean { + contains(key: BindingAddress): boolean { key = BindingKey.validate(key); return this.registry.has(key); } @@ -227,7 +223,7 @@ export class Context { * Check if a key is bound in the context or its ancestors * @param key Binding key */ - isBound(key: BindingAddress): boolean { + isBound(key: BindingAddress): boolean { if (this.contains(key)) return true; if (this._parent) { return this._parent.isBound(key); @@ -239,7 +235,7 @@ export class Context { * Get the owning context for a binding key * @param key Binding key */ - getOwnerContext(key: BindingAddress): Context | undefined { + getOwnerContext(key: BindingAddress): Context | undefined { if (this.contains(key)) return this; if (this._parent) { return this._parent.getOwnerContext(key); @@ -271,12 +267,7 @@ export class Context { pattern?: string | RegExp | BindingFilter, ): Readonly>[] { const bindings: Readonly[] = []; - const filter: BindingFilter = - pattern == null || - typeof pattern === 'string' || - pattern instanceof RegExp - ? bindingKeyFilter(pattern) - : pattern; + const filter = filterByKey(pattern); for (const b of this.registry.values()) { if (filter(b)) bindings.push(b); @@ -303,7 +294,7 @@ export class Context { findByTag( tagFilter: BindingTag | RegExp, ): Readonly>[] { - return this.find(bindingTagFilter(tagFilter)); + return this.find(filterByTag(tagFilter)); } protected _mergeWithParent( diff --git a/packages/context/src/inject.ts b/packages/context/src/inject.ts index 5b149359feb8..e5741719cea2 100644 --- a/packages/context/src/inject.ts +++ b/packages/context/src/inject.ts @@ -13,7 +13,7 @@ import { PropertyDecoratorFactory, } from '@loopback/metadata'; import {BindingTag} from './binding'; -import {BindingFilter, bindingTagFilter} from './binding-filter'; +import {BindingFilter, filterByTag} from './binding-filter'; import {BindingAddress} from './binding-key'; import {Context} from './context'; import {ContextView} from './context-view'; @@ -273,7 +273,7 @@ export namespace inject { {decorator: '@inject.tag', tag: bindingTag}, metadata, ); - return inject(bindingTagFilter(bindingTag), metadata); + return inject(filterByTag(bindingTag), metadata); }; /** @@ -281,7 +281,7 @@ export namespace inject { * * ```ts * class MyControllerWithView { - * @inject.view(Context.bindingTagFilter('foo')) + * @inject.view(filterByTag('foo')) * view: ContextView; * } * ``` @@ -318,8 +318,10 @@ function resolveAsGetter( ) { const targetType = inspectTargetType(injection); if (targetType && targetType !== Function) { + const targetName = ResolutionSession.describeInjection(injection)! + .targetName; throw new Error( - `The target type ${targetType.name} is not a Getter function`, + `The type of ${targetName} (${targetType.name}) is not a Getter function`, ); } if (typeof injection.bindingSelector === 'function') { @@ -328,7 +330,8 @@ function resolveAsGetter( // We need to clone the session for the getter as it will be resolved later session = ResolutionSession.fork(session); return function getter() { - return ctx.get(injection.bindingSelector as BindingAddress, { + const key = injection.bindingSelector as BindingAddress; + return ctx.get(key, { session, optional: injection.metadata && injection.metadata.optional, }); @@ -338,13 +341,16 @@ function resolveAsGetter( function resolveAsSetter(ctx: Context, injection: Injection) { const targetType = inspectTargetType(injection); if (targetType && targetType !== Function) { + const targetName = ResolutionSession.describeInjection(injection)! + .targetName; throw new Error( - `The target type ${targetType.name} is not a Setter function`, + `The type of ${targetName} (${targetType.name}) is not a Setter function`, ); } // No resolution session should be propagated into the setter return function setter(value: unknown) { - ctx.bind(injection.bindingSelector as BindingAddress).to(value); + const key = injection.bindingSelector as BindingAddress; + ctx.bind(key).to(value); }; } @@ -417,7 +423,11 @@ function resolveByFilter( const targetType = inspectTargetType(injection); if (injection.metadata && injection.metadata.decorator === '@inject.view') { if (targetType && targetType !== ContextView) { - throw new Error(`The target type ${targetType.name} is not ContextView`); + const targetName = ResolutionSession.describeInjection(injection)! + .targetName; + throw new Error( + `The type of ${targetName} (${targetType.name}) is not ContextView`, + ); } } const bindingFilter = injection.bindingSelector as BindingFilter; diff --git a/packages/context/src/resolver.ts b/packages/context/src/resolver.ts index 4d22c75e6b35..f4a7656d6459 100644 --- a/packages/context/src/resolver.ts +++ b/packages/context/src/resolver.ts @@ -101,15 +101,13 @@ function resolve( return injection.resolve(ctx, injection, s); } else { // Default to resolve the value from the context by binding key - return ctx.getValueOrPromise( - injection.bindingSelector as BindingAddress, - { - session: s, - // If the `optional` flag is set for the injection, the resolution - // will return `undefined` instead of throwing an error - optional: injection.metadata && injection.metadata.optional, - }, - ); + const key = injection.bindingSelector as BindingAddress; + return ctx.getValueOrPromise(key, { + session: s, + // If the `optional` flag is set for the injection, the resolution + // will return `undefined` instead of throwing an error + optional: injection.metadata && injection.metadata.optional, + }); } }, injection, diff --git a/packages/context/test/acceptance/class-level-bindings.acceptance.ts b/packages/context/test/acceptance/class-level-bindings.acceptance.ts index e7f3b530d88b..e5dd06edc372 100644 --- a/packages/context/test/acceptance/class-level-bindings.acceptance.ts +++ b/packages/context/test/acceptance/class-level-bindings.acceptance.ts @@ -190,7 +190,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { ctx.bind('store').toClass(Store); expect(() => ctx.getSync('store')).to.throw( - /The target type String is not a Getter function/, + 'The type of Store.constructor[0] (String) is not a Getter function', ); }); @@ -203,7 +203,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { ctx.bind('store').toClass(Store); expect(() => ctx.getSync('store')).to.throw( - /The target type Object is not a Setter function/, + 'The type of Store.constructor[0] (Object) is not a Setter function', ); }); diff --git a/packages/context/test/acceptance/context-view.acceptance.ts b/packages/context/test/acceptance/context-view.acceptance.ts index 514629ffbfef..4c48cff33947 100644 --- a/packages/context/test/acceptance/context-view.acceptance.ts +++ b/packages/context/test/acceptance/context-view.acceptance.ts @@ -1,7 +1,7 @@ import {expect} from '@loopback/testlab'; import { Binding, - bindingTagFilter, + filterByTag, Context, ContextEventListener, ContextEventType, @@ -31,7 +31,7 @@ describe('ContextView - watches matching bindings', () => { function givenControllerWatcher() { server = givenServerWithinAnApp(); - contextWatcher = server.createView(bindingTagFilter('controller')); + contextWatcher = server.createView(filterByTag('controller')); givenController(server, '1'); givenController(server.parent!, '2'); } @@ -57,19 +57,19 @@ describe('@inject.* - injects a live collection of matching bindings', async () beforeEach(givenPrimeNumbers); class MyControllerWithGetter { - @inject.getter(bindingTagFilter('prime'), {watch: true}) + @inject.getter(filterByTag('prime'), {watch: true}) getter: Getter; } class MyControllerWithValues { constructor( - @inject(bindingTagFilter('prime')) + @inject(filterByTag('prime')) public values: number[], ) {} } class MyControllerWithView { - @inject.view(bindingTagFilter('prime')) + @inject.view(filterByTag('prime')) view: ContextView; } @@ -139,7 +139,7 @@ describe('ContextEventListener - listens on matching bindings', () => { class MyListenerForControllers implements ContextEventListener { controllers: Set = new Set(); - filter = bindingTagFilter('controller'); + filter = filterByTag('controller'); listen(event: ContextEventType, binding: Readonly>) { if (event === 'bind') { this.controllers.add(binding.tagMap.name); diff --git a/packages/context/test/unit/context-view.unit.ts b/packages/context/test/unit/context-view.unit.ts index 70e1934b2f56..391bcd390328 100644 --- a/packages/context/test/unit/context-view.unit.ts +++ b/packages/context/test/unit/context-view.unit.ts @@ -4,13 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import { - Binding, - BindingScope, - bindingTagFilter, - Context, - ContextView, -} from '../..'; +import {Binding, BindingScope, filterByTag, Context, ContextView} from '../..'; describe('ContextView', () => { let ctx: Context; @@ -97,7 +91,7 @@ describe('ContextView', () => { function givenContextView() { bindings = []; ctx = givenContext(bindings); - contextView = ctx.createView(bindingTagFilter('foo')); + contextView = ctx.createView(filterByTag('foo')); } }); diff --git a/packages/context/test/unit/inject-view.unit.ts b/packages/context/test/unit/inject-view.unit.ts index d8f6034bc6cb..ffe6ea3df835 100644 --- a/packages/context/test/unit/inject-view.unit.ts +++ b/packages/context/test/unit/inject-view.unit.ts @@ -7,7 +7,7 @@ import {expect} from '@loopback/testlab'; import { Binding, BindingScope, - bindingTagFilter, + filterByTag, Context, ContextView, Getter, @@ -19,19 +19,19 @@ describe('@inject.view', async () => { beforeEach(() => (ctx = givenContext())); class MyControllerWithGetter { - @inject.view(bindingTagFilter('foo'), {watch: true}) + @inject.view(filterByTag('foo'), {watch: true}) getter: Getter; } class MyControllerWithValues { constructor( - @inject.view(bindingTagFilter('foo')) + @inject.view(filterByTag('foo')) public values: string[], ) {} } class MyControllerWithTracker { - @inject.view(bindingTagFilter('foo')) + @inject.view(filterByTag('foo')) view: ContextView; } @@ -39,14 +39,18 @@ describe('@inject.view', async () => { ctx.bind('my-controller').toClass(MyControllerWithGetter); await expect( ctx.get('my-controller'), - ).to.be.rejectedWith('The target type Function is not ContextView'); + ).to.be.rejectedWith( + 'The type of MyControllerWithGetter.prototype.getter (Function) is not ContextView', + ); }); it('reports error if the target type (string[]) is not ContextView', async () => { ctx.bind('my-controller').toClass(MyControllerWithValues); await expect( ctx.get('my-controller'), - ).to.be.rejectedWith('The target type Array is not ContextView'); + ).to.be.rejectedWith( + 'The type of MyControllerWithValues.constructor[0] (Array) is not ContextView', + ); }); it('injects as a view', async () => { @@ -69,19 +73,19 @@ describe('@inject with filter function', async () => { beforeEach(() => (ctx = givenContext())); class MyControllerWithGetter { - @inject.getter(bindingTagFilter('foo'), {watch: true}) + @inject.getter(filterByTag('foo'), {watch: true}) getter: Getter; } class MyControllerWithValues { constructor( - @inject(bindingTagFilter('foo')) + @inject(filterByTag('foo')) public values: string[], ) {} } class MyControllerWithView { - @inject(bindingTagFilter('foo')) + @inject(filterByTag('foo')) view: ContextView; }