diff --git a/packages/authentication/test/unit/authenticate-action.test.ts b/packages/authentication/test/unit/authenticate-action.test.ts index 8d5eb008379d..f4cd6ba05dd1 100644 --- a/packages/authentication/test/unit/authenticate-action.test.ts +++ b/packages/authentication/test/unit/authenticate-action.test.ts @@ -60,10 +60,10 @@ describe('AuthenticationProvider', () => { .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticationProvider); const request = {}; - const authenticate = await context.get( + const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); - const user: UserProfile = await authenticate(request); + const user: UserProfile | undefined = await authenticate(request); expect(user).to.be.equal(mockUser); }); @@ -73,7 +73,7 @@ describe('AuthenticationProvider', () => { context .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticationProvider); - const authenticate = await context.get( + const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); const request = {}; @@ -92,7 +92,7 @@ describe('AuthenticationProvider', () => { context .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticationProvider); - const authenticate = await context.get( + const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); const request = {}; diff --git a/packages/boot/src/bootstrapper.ts b/packages/boot/src/bootstrapper.ts index dbd79c3ea709..9b2faf6ede98 100644 --- a/packages/boot/src/bootstrapper.ts +++ b/packages/boot/src/bootstrapper.ts @@ -109,7 +109,10 @@ export class Bootstrapper { // Resolve Booter Instances const booterInsts = await resolveList(filteredBindings, binding => - bootCtx.get(binding.key), + // We cannot use Booter interface here because "filter.booters" + // allows arbitrary string values, not only the phases defined + // by Booter interface + bootCtx.get<{[phase: string]: () => Promise}>(binding.key), ); // Run phases of booters diff --git a/packages/boot/test/unit/bootstrapper.unit.ts b/packages/boot/test/unit/bootstrapper.unit.ts index d4f94e3aa9ed..9876225e7d21 100644 --- a/packages/boot/test/unit/bootstrapper.unit.ts +++ b/packages/boot/test/unit/bootstrapper.unit.ts @@ -16,49 +16,50 @@ describe('boot-strapper unit tests', () => { let app: BootApp; let bootstrapper: Bootstrapper; const booterKey = `${BootBindings.BOOTER_PREFIX}.TestBooter`; - const booterKey2 = `${BootBindings.BOOTER_PREFIX}.TestBooter2`; + const anotherBooterKey = `${BootBindings.BOOTER_PREFIX}.AnotherBooter`; beforeEach(getApplication); beforeEach(getBootStrapper); it('finds and runs registered booters', async () => { const ctx = await bootstrapper.boot(); - const booterInst = await ctx.get(booterKey); - expect(booterInst.configureCalled).to.be.True(); - expect(booterInst.loadCalled).to.be.True(); + const booterInst = await ctx.get(booterKey); + expect(booterInst.phasesCalled).to.eql([ + 'TestBooter:configure', + 'TestBooter:load', + ]); }); it('binds booters passed in BootExecutionOptions', async () => { - const ctx = await bootstrapper.boot({booters: [TestBooter2]}); - const booterInst2 = await ctx.get(booterKey2); - expect(booterInst2).to.be.instanceof(TestBooter2); - expect(booterInst2.configureCalled).to.be.True(); + const ctx = await bootstrapper.boot({booters: [AnotherBooter]}); + const anotherBooterInst = await ctx.get(anotherBooterKey); + expect(anotherBooterInst).to.be.instanceof(AnotherBooter); + expect(anotherBooterInst.phasesCalled).to.eql(['AnotherBooter:configure']); }); it('no booters run when BootOptions.filter.booters is []', async () => { const ctx = await bootstrapper.boot({filter: {booters: []}}); - const booterInst = await ctx.get(booterKey); - expect(booterInst.configureCalled).to.be.False(); - expect(booterInst.loadCalled).to.be.False(); + const booterInst = await ctx.get(booterKey); + expect(booterInst.phasesCalled).to.eql([]); }); it('only runs booters passed in via BootOptions.filter.booters', async () => { const ctx = await bootstrapper.boot({ - booters: [TestBooter2], - filter: {booters: ['TestBooter2']}, + booters: [AnotherBooter], + filter: {booters: ['AnotherBooter']}, }); - const booterInst = await ctx.get(booterKey); - const booterInst2 = await ctx.get(booterKey2); - expect(booterInst.configureCalled).to.be.False(); - expect(booterInst.loadCalled).to.be.False(); - expect(booterInst2.configureCalled).to.be.True(); + const testBooterInst = await ctx.get(booterKey); + const anotherBooterInst = await ctx.get(anotherBooterKey); + const phasesCalled = testBooterInst.phasesCalled.concat( + anotherBooterInst.phasesCalled, + ); + expect(phasesCalled).to.eql(['AnotherBooter:configure']); }); it('only runs phases passed in via BootOptions.filter.phases', async () => { const ctx = await bootstrapper.boot({filter: {phases: ['configure']}}); - const booterInst = await ctx.get(booterKey); - expect(booterInst.configureCalled).to.be.True(); - expect(booterInst.loadCalled).to.be.False(); + const booterInst = await ctx.get(booterKey); + expect(booterInst.phasesCalled).to.eql(['TestBooter:configure']); }); /** @@ -80,8 +81,8 @@ describe('boot-strapper unit tests', () => { * A TestBooter for testing purposes. Implements configure and load. */ class TestBooter implements Booter { - configureCalled = false; - loadCalled = false; + private configureCalled = false; + private loadCalled = false; async configure() { this.configureCalled = true; } @@ -89,15 +90,28 @@ describe('boot-strapper unit tests', () => { async load() { this.loadCalled = true; } + + get phasesCalled() { + const result = []; + if (this.configureCalled) result.push('TestBooter:configure'); + if (this.loadCalled) result.push('TestBooter:load'); + return result; + } } /** * A TestBooter for testing purposes. Implements configure. */ - class TestBooter2 implements Booter { - configureCalled = false; + class AnotherBooter implements Booter { + private configureCalled = false; async configure() { this.configureCalled = true; } + + get phasesCalled() { + const result = []; + if (this.configureCalled) result.push('AnotherBooter:configure'); + return result; + } } }); diff --git a/packages/build/config/tslint.build.json b/packages/build/config/tslint.build.json index 84e7890b1b0e..00b53823746a 100644 --- a/packages/build/config/tslint.build.json +++ b/packages/build/config/tslint.build.json @@ -12,7 +12,10 @@ // These rules catch common errors in JS programming or otherwise // confusing constructs that are prone to producing bugs. - "await-promise": true, + // User-land promises like Bluebird implement "PromiseLike" (not "Promise") + // interface only. The string "PromiseLike" bellow is needed to + // tell tslint that it's ok to `await` such promises. + "await-promise": [true, "PromiseLike"], "no-floating-promises": true, "no-unused-variable": true, "no-void-expression": [true, "ignore-arrow-function-shorthand"] diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index b3644c3f8e79..6bdd02cbf3c6 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -8,7 +8,7 @@ import {ResolutionSession} from './resolution-session'; import {instantiateClass} from './resolver'; import { Constructor, - isPromise, + isPromiseLike, BoundValue, ValueOrPromise, } from './value-promise'; @@ -176,7 +176,7 @@ export class Binding { ): ValueOrPromise { // Initialize the cache as a weakmap keyed by context if (!this._cache) this._cache = new WeakMap(); - if (isPromise(result)) { + if (isPromiseLike(result)) { if (this.scope === BindingScope.SINGLETON) { // Cache the value at owning context level result = result.then(val => { @@ -294,7 +294,7 @@ export class Binding { * ``` */ to(value: BoundValue): this { - if (isPromise(value)) { + 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. @@ -380,7 +380,7 @@ export class Binding { ctx!, session, ); - if (isPromise(providerOrPromise)) { + if (isPromiseLike(providerOrPromise)) { return providerOrPromise.then(p => p.value()); } else { return providerOrPromise.value(); diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index 9222e4eecfff..e3c800492f9e 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -4,17 +4,13 @@ // License text available at https://opensource.org/licenses/MIT import {Binding} from './binding'; -import { - isPromise, - BoundValue, - ValueOrPromise, - getDeepProperty, -} from './value-promise'; +import {isPromiseLike, getDeepProperty} from './value-promise'; import {ResolutionOptions, ResolutionSession} from './resolution-session'; import {v1 as uuidv1} from 'uuid'; import * as debugModule from 'debug'; +import {ValueOrPromise} from '.'; const debug = debugModule('loopback:context'); /** @@ -205,44 +201,69 @@ export class Context { } /** - * Get the value bound to the given key, optionally return a (deep) property - * of the bound value. + * Get the value bound to the given key, throw an error when no value was + * bound for the given key. * * @example * * ```ts * // get the value bound to "application.instance" - * const app = await ctx.get('application.instance'); + * const app = await ctx.get('application.instance'); * * // get "rest" property from the value bound to "config" - * const config = await ctx.getValueOrPromise('config#rest'); + * const config = await ctx.get('config#rest'); * * // get "a" property of "numbers" property from the value bound to "data" * ctx.bind('data').to({numbers: {a: 1, b: 2}, port: 3000}); - * const a = await ctx.get('data#numbers.a'); + * const a = await ctx.get('data#numbers.a'); + * ``` + * + * @param keyWithPath The binding key, optionally suffixed with a path to the + * (deeply) nested property to retrieve. + * @returns A promise of the bound value. + */ + get(keyWithPath: string): Promise; + + /** + * Get the value bound to the given key, optionally return a (deep) property + * of the bound value. + * + * @example + * + * ```ts + * // get "rest" property from the value bound to "config" + * // use "undefined" when not config was provided + * const config = await ctx.get('config#rest', { + * optional: true + * }); * ``` * * @param keyWithPath The binding key, optionally suffixed with a path to the * (deeply) nested property to retrieve. * @param optionsOrSession Options or session for resolution. An instance of * `ResolutionSession` is accepted for backward compatibility. - * @returns A promise of the bound value. + * @returns A promise of the bound value, or a promise of undefined when + * the optional binding was not found. */ - get( + get( + keyWithPath: string, + optionsOrSession?: ResolutionOptions | ResolutionSession, + ): Promise; + + // Implementation + async get( keyWithPath: string, optionsOrSession?: ResolutionOptions | ResolutionSession, - ): Promise { + ): Promise { /* istanbul ignore if */ if (debug.enabled) { debug('Resolving binding: %s', keyWithPath); } - try { - return Promise.resolve( - this.getValueOrPromise(keyWithPath, optionsOrSession), - ); - } catch (err) { - return Promise.reject(err); - } + + return await this.getValueOrPromise( + keyWithPath, + optionsOrSession, + ); } /** @@ -257,10 +278,10 @@ export class Context { * * ```ts * // get the value bound to "application.instance" - * const app = ctx.get('application.instance'); + * const app = ctx.getSync('application.instance'); * * // get "rest" property from the value bound to "config" - * const config = ctx.getValueOrPromise('config#rest'); + * const config = await ctx.getSync('config#rest'); * ``` * * @param keyWithPath The binding key, optionally suffixed with a path to the @@ -269,20 +290,52 @@ export class Context { * `ResolutionSession` is accepted for backward compatibility. * @returns A promise of the bound value. */ - getSync( + getSync(keyWithPath: string): T; + + /** + * Get the synchronous value bound to the given key, optionally + * return a (deep) property of the bound value. + * + * This method throws an error if the bound value requires async computation + * (returns a promise). You should never rely on sync bindings in production + * code. + * + * @example + * + * ```ts + * // get "rest" property from the value bound to "config" + * // use "undefined" when no config was provided + * const config = await ctx.getSync('config#rest', { + * optional: true + * }); + * ``` + * + * @param keyWithPath The binding key, optionally suffixed with a path to the + * (deeply) nested property to retrieve. + * * @param optionsOrSession Options or session for resolution. An instance of + * `ResolutionSession` is accepted for backward compatibility. + * @returns The bound value, or undefined when an optional binding was not found. + */ + getSync( keyWithPath: string, optionsOrSession?: ResolutionOptions | ResolutionSession, - ): BoundValue { + ): T | undefined; + + // Implementation + getSync( + keyWithPath: string, + optionsOrSession?: ResolutionOptions | ResolutionSession, + ): T | undefined { /* istanbul ignore if */ if (debug.enabled) { debug('Resolving binding synchronously: %s', keyWithPath); } - const valueOrPromise = this.getValueOrPromise( + const valueOrPromise = this.getValueOrPromise( keyWithPath, optionsOrSession, ); - if (isPromise(valueOrPromise)) { + if (isPromiseLike(valueOrPromise)) { throw new Error( `Cannot get ${keyWithPath} synchronously: the value is a promise`, ); @@ -336,14 +389,14 @@ export class Context { * * ```ts * // get the value bound to "application.instance" - * ctx.getValueOrPromise('application.instance'); + * ctx.getValueOrPromise('application.instance'); * * // get "rest" property from the value bound to "config" - * ctx.getValueOrPromise('config#rest'); + * ctx.getValueOrPromise('config#rest'); * * // get "a" property of "numbers" property from the value bound to "data" * ctx.bind('data').to({numbers: {a: 1, b: 2}, port: 3000}); - * ctx.getValueOrPromise('data#numbers.a'); + * ctx.getValueOrPromise('data#numbers.a'); * ``` * * @param keyWithPath The binding key, optionally suffixed with a path to the @@ -353,16 +406,20 @@ export class Context { * on how the binding was configured. * @internal */ - getValueOrPromise( + getValueOrPromise( keyWithPath: string, optionsOrSession?: ResolutionOptions | ResolutionSession, - ): ValueOrPromise { + ): ValueOrPromise { const {key, path} = Binding.parseKeyWithPath(keyWithPath); + + // backwards compatibility if (optionsOrSession instanceof ResolutionSession) { optionsOrSession = {session: optionsOrSession}; } + const binding = this.getBinding(key, optionsOrSession); if (binding == null) return undefined; + const boundValue = binding.getValue( this, optionsOrSession && optionsOrSession.session, @@ -371,11 +428,11 @@ export class Context { return boundValue; } - if (isPromise(boundValue)) { - return boundValue.then(v => getDeepProperty(v, path)); + if (isPromiseLike(boundValue)) { + return boundValue.then(v => getDeepProperty(v, path) as T); } - return getDeepProperty(boundValue, path); + return getDeepProperty(boundValue, path) as T; } /** diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 24f11013053f..101f1f570fc0 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -6,7 +6,7 @@ export * from '@loopback/metadata'; export { - isPromise, + isPromiseLike, BoundValue, Constructor, ValueOrPromise, diff --git a/packages/context/src/resolver.ts b/packages/context/src/resolver.ts index eac39862c64a..f41eb708361c 100644 --- a/packages/context/src/resolver.ts +++ b/packages/context/src/resolver.ts @@ -10,7 +10,7 @@ import { Constructor, ValueOrPromise, MapObject, - isPromise, + isPromiseLike, resolveList, resolveMap, } from './value-promise'; @@ -57,7 +57,7 @@ export function instantiateClass( const argsOrPromise = resolveInjectedArguments(ctor, '', ctx, session); const propertiesOrPromise = resolveInjectedProperties(ctor, ctx, session); let inst: ValueOrPromise; - if (isPromise(argsOrPromise)) { + if (isPromiseLike(argsOrPromise)) { // Instantiate the class asynchronously inst = argsOrPromise.then(args => { /* istanbul ignore if */ @@ -74,13 +74,13 @@ export function instantiateClass( // Instantiate the class synchronously inst = new ctor(...argsOrPromise); } - if (isPromise(propertiesOrPromise)) { + if (isPromiseLike(propertiesOrPromise)) { return propertiesOrPromise.then(props => { /* istanbul ignore if */ if (debug.enabled) { debug('Injected properties for %s:', ctor.name, props); } - if (isPromise(inst)) { + if (isPromiseLike(inst)) { // Inject the properties asynchronously return inst.then(obj => Object.assign(obj, props)); } else { @@ -89,7 +89,7 @@ export function instantiateClass( } }); } else { - if (isPromise(inst)) { + if (isPromiseLike(inst)) { /* istanbul ignore if */ if (debug.enabled) { debug('Injected properties for %s:', ctor.name, propertiesOrPromise); @@ -257,7 +257,7 @@ export function invokeMethod( typeof targetWithMethods[method] === 'function', `Method ${method} not found`, ); - if (isPromise(argsOrPromise)) { + if (isPromiseLike(argsOrPromise)) { // Invoke the target method asynchronously return argsOrPromise.then(args => { /* istanbul ignore if */ diff --git a/packages/context/src/value-promise.ts b/packages/context/src/value-promise.ts index ceb212dc1106..114e473c86d0 100644 --- a/packages/context/src/value-promise.ts +++ b/packages/context/src/value-promise.ts @@ -21,8 +21,13 @@ export type BoundValue = any; /** * Representing a value or promise. This type is used to represent results of * synchronous/asynchronous resolution of values. + * + * Note that we are using PromiseLike instead of native Promise to describe + * the asynchronous variant. This allows producers of async values to use + * any Promise implementation (e.g. Bluebird) instead of native Promises + * provided by JavaScript runtime. */ -export type ValueOrPromise = T | Promise; +export type ValueOrPromise = T | PromiseLike; export type MapObject = {[name: string]: T}; @@ -32,8 +37,8 @@ export type MapObject = {[name: string]: T}; * * @param value The value to check. */ -export function isPromise( - value: T | PromiseLike, +export function isPromiseLike( + value: T | PromiseLike | undefined, ): value is PromiseLike { if (!value) return false; if (typeof value !== 'object' && typeof value !== 'function') return false; @@ -45,7 +50,7 @@ export function isPromise( * @param value Value of an object * @param path Path to the property */ -export function getDeepProperty(value: BoundValue, path: string) { +export function getDeepProperty(value: BoundValue, path: string): BoundValue { const props = path.split('.').filter(Boolean); for (const p of props) { if (value == null) { @@ -99,7 +104,7 @@ export function resolveMap( for (const key in map) { const valueOrPromise = resolver(map[key], key, map); - if (isPromise(valueOrPromise)) { + if (isPromiseLike(valueOrPromise)) { if (!asyncResolvers) asyncResolvers = []; asyncResolvers.push(valueOrPromise.then(setter(key))); } else { @@ -158,7 +163,7 @@ export function resolveList( // tslint:disable-next-line:prefer-for-of for (let ix = 0; ix < list.length; ix++) { const valueOrPromise = resolver(list[ix], ix, list); - if (isPromise(valueOrPromise)) { + if (isPromiseLike(valueOrPromise)) { if (!asyncResolvers) asyncResolvers = []; asyncResolvers.push(valueOrPromise.then(setter(ix))); } else { @@ -190,7 +195,7 @@ export function tryWithFinally( finalAction(); throw err; } - if (isPromise(result)) { + if (isPromiseLike(result)) { // Once (promise.finally)[https://github.com/tc39/proposal-promise-finally // is supported, the following can be simplifed as // `result = result.finally(finalAction);` diff --git a/packages/context/test/acceptance/class-level-bindings.ts b/packages/context/test/acceptance/class-level-bindings.ts index 8dd0a9be334f..e4f79b8239fc 100644 --- a/packages/context/test/acceptance/class-level-bindings.ts +++ b/packages/context/test/acceptance/class-level-bindings.ts @@ -150,7 +150,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { } ctx.bind('store').toClass(Store); - const store = ctx.getSync('store'); + const store = ctx.getSync('store'); expect(store.getter).to.be.Function(); expect(await store.getter()).to.equal('value'); @@ -166,7 +166,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { } ctx.bind('store').toClass(Store); - const store = ctx.getSync('store'); + const store = ctx.getSync('store'); expect(store.setter).to.be.Function(); store.setter('a-value'); @@ -181,7 +181,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { ctx.bind('config').to({test: 'test-config'}); ctx.bind('component').toClass(TestComponent); - const resolved = await ctx.get('component'); + const resolved = await ctx.get('component'); expect(resolved.config).to.equal('test-config'); }); @@ -257,7 +257,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { .bind('store.locations.sj') .toDynamicValue(async () => 'San Jose') .tag('store:location'); - const store: Store = await ctx.get('store'); + const store = await ctx.get('store'); expect(store.locations).to.eql(['San Francisco', 'San Jose']); }); @@ -292,7 +292,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { .bind('store.locations.sj') .toProvider(LocationProvider) .tag('store:location'); - const store: Store = await ctx.get('store'); + const store = await ctx.get('store'); expect(store.locations).to.eql(['San Francisco', 'San Jose']); expect(resolutionPath).to.eql( 'store --> @Store.constructor[0] --> store.locations.sj --> ' + diff --git a/packages/context/test/unit/binding.ts b/packages/context/test/unit/binding.ts index e0ce001440e3..aab0facfeb74 100644 --- a/packages/context/test/unit/binding.ts +++ b/packages/context/test/unit/binding.ts @@ -125,7 +125,7 @@ describe('Binding', () => { describe('toDynamicValue(dynamicValueFn)', () => { it('support a factory', async () => { const b = ctx.bind('msg').toDynamicValue(() => Promise.resolve('hello')); - const value: string = await ctx.get('msg'); + const value = await ctx.get('msg'); expect(value).to.equal('hello'); expect(b.type).to.equal(BindingType.DYNAMIC_VALUE); }); @@ -136,7 +136,7 @@ describe('Binding', () => { ctx.bind('msg').toDynamicValue(() => Promise.resolve('world')); const b = ctx.bind('myService').toClass(MyService); expect(b.type).to.equal(BindingType.CLASS); - const myService: MyService = await ctx.get('myService'); + const myService = await ctx.get('myService'); expect(myService.getMessage()).to.equal('hello world'); }); }); @@ -145,7 +145,7 @@ describe('Binding', () => { it('binding returns the expected value', async () => { ctx.bind('msg').to('hello'); ctx.bind('provider_key').toProvider(MyProvider); - const value: string = await ctx.get('provider_key'); + const value = await ctx.get('provider_key'); expect(value).to.equal('hello world'); }); @@ -159,7 +159,7 @@ describe('Binding', () => { it('support asynchronous dependencies of provider class', async () => { ctx.bind('msg').toDynamicValue(() => Promise.resolve('hello')); ctx.bind('provider_key').toProvider(MyProvider); - const value: string = await ctx.get('provider_key'); + const value = await ctx.get('provider_key'); expect(value).to.equal('hello world'); }); diff --git a/packages/context/test/unit/context.ts b/packages/context/test/unit/context.ts index a2f87c9cff83..2299d018ad21 100644 --- a/packages/context/test/unit/context.ts +++ b/packages/context/test/unit/context.ts @@ -4,7 +4,13 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {Context, Binding, BindingScope, BindingType, isPromise} from '../..'; +import { + Context, + Binding, + BindingScope, + BindingType, + isPromiseLike, +} from '../..'; /** * Create a subclass of context so that we can access parents and registry @@ -493,8 +499,8 @@ describe('Context', () => { it('returns promise for async values', async () => { ctx.bind('key').toDynamicValue(() => Promise.resolve('value')); - const valueOrPromise = ctx.getValueOrPromise('key'); - expect(isPromise(valueOrPromise)).to.be.true(); + const valueOrPromise = ctx.getValueOrPromise('key'); + expect(isPromiseLike(valueOrPromise)).to.be.true(); const value = await valueOrPromise; expect(value).to.equal('value'); }); @@ -509,7 +515,9 @@ describe('Context', () => { ctx .bind('key') .toDynamicValue(() => Promise.resolve({test: 'test-value'})); - const value = await ctx.getValueOrPromise('key#test'); + const valueOrPromise = ctx.getValueOrPromise('key#test'); + expect(isPromiseLike(valueOrPromise)).to.be.true(); + const value = await valueOrPromise; expect(value).to.equal('test-value'); }); diff --git a/packages/context/test/unit/is-promise.test.ts b/packages/context/test/unit/is-promise.test.ts index 14f6cf7e96c3..1de3409492da 100644 --- a/packages/context/test/unit/is-promise.test.ts +++ b/packages/context/test/unit/is-promise.test.ts @@ -5,38 +5,38 @@ import * as bluebird from 'bluebird'; import {expect} from '@loopback/testlab'; -import {isPromise} from '../..'; +import {isPromiseLike} from '../..'; describe('isPromise', () => { it('returns false for undefined', () => { - expect(isPromise(undefined)).to.be.false(); + expect(isPromiseLike(undefined)).to.be.false(); }); it('returns false for a string value', () => { - expect(isPromise('string-value')).to.be.false(); + expect(isPromiseLike('string-value')).to.be.false(); }); it('returns false for a plain object', () => { - expect(isPromise({foo: 'bar'})).to.be.false(); + expect(isPromiseLike({foo: 'bar'})).to.be.false(); }); it('returns false for an array', () => { - expect(isPromise([1, 2, 3])).to.be.false(); + expect(isPromiseLike([1, 2, 3])).to.be.false(); }); it('returns false for a Date', () => { - expect(isPromise(new Date())).to.be.false(); + expect(isPromiseLike(new Date())).to.be.false(); }); it('returns true for a native Promise', () => { - expect(isPromise(Promise.resolve())).to.be.true(); + expect(isPromiseLike(Promise.resolve())).to.be.true(); }); it('returns true for a Bluebird Promise', () => { - expect(isPromise(bluebird.resolve())).to.be.true(); + expect(isPromiseLike(bluebird.resolve())).to.be.true(); }); it('returns false when .then() is not a function', () => { - expect(isPromise({then: 'later'})).to.be.false(); + expect(isPromiseLike({then: 'later'})).to.be.false(); }); }); diff --git a/packages/example-rpc-server/src/servers/rpc-router.ts b/packages/example-rpc-server/src/servers/rpc-router.ts index 4340c4f17e0b..88e7742da7a5 100644 --- a/packages/example-rpc-server/src/servers/rpc-router.ts +++ b/packages/example-rpc-server/src/servers/rpc-router.ts @@ -22,9 +22,9 @@ export async function routeHandler( const ctrl = request.body.controller; const method = request.body.method; const input = request.body.input; - let controller; + let controller: Controller; try { - controller = await server.get(`controllers.${ctrl}`); + controller = await server.get(`controllers.${ctrl}`); if (!controller[method]) { throw new Error( `No method was found on controller "${ctrl}" with name "${method}".`, diff --git a/packages/repository/src/decorators/repository.ts b/packages/repository/src/decorators/repository.ts index 6b7819407a73..5d0f7e3ff618 100644 --- a/packages/repository/src/decorators/repository.ts +++ b/packages/repository/src/decorators/repository.ts @@ -139,7 +139,9 @@ async function resolve(ctx: Context, injection: Injection) { let dataSource = meta.dataSource; if (meta.dataSourceName) { - dataSource = await ctx.get('datasources.' + meta.dataSourceName); + dataSource = await ctx.get( + 'datasources.' + meta.dataSourceName, + ); } assert( dataSource instanceof DataSourceConstructor, diff --git a/packages/repository/src/legacy-juggler-bridge.ts b/packages/repository/src/legacy-juggler-bridge.ts index 23205ccd66ea..50f5815e4c5e 100644 --- a/packages/repository/src/legacy-juggler-bridge.ts +++ b/packages/repository/src/legacy-juggler-bridge.ts @@ -6,7 +6,7 @@ export const jugglerModule = require('loopback-datasource-juggler'); import * as assert from 'assert'; -import {isPromise} from '@loopback/context'; +import {isPromiseLike} from '@loopback/context'; import {DataObject, Options} from './common-types'; import {Entity} from './model'; import {Filter, Where} from './query'; @@ -51,8 +51,11 @@ export function bindModel( */ /* tslint:disable-next-line:no-any */ function ensurePromise(p: juggler.PromiseOrVoid): Promise { - if (p && isPromise(p)) { - return p; + if (p && isPromiseLike(p)) { + // Juggler uses promise-like Bluebird instead of native Promise + // implementation. We need to convert the promise returned by juggler + // methods to proper native Promise instance. + return Promise.resolve(p); } else { return Promise.reject(new Error('The value should be a Promise: ' + p)); } diff --git a/packages/repository/src/loopback-datasource-juggler.ts b/packages/repository/src/loopback-datasource-juggler.ts index f14c0a7ecfc2..e16b6755ad75 100644 --- a/packages/repository/src/loopback-datasource-juggler.ts +++ b/packages/repository/src/loopback-datasource-juggler.ts @@ -11,9 +11,11 @@ import {EventEmitter} from 'events'; export declare namespace juggler { /** - * Return type for promisified Node.js async methods + * Return type for promisified Node.js async methods. + * + * Note that juggler uses Bluebird, not the native Promise. */ - export type PromiseOrVoid = Promise | void; + export type PromiseOrVoid = PromiseLike | void; /** * Property definition diff --git a/packages/repository/test/unit/decorator/repository-with-di.ts b/packages/repository/test/unit/decorator/repository-with-di.ts index 12809d4e8723..df9c880abe8d 100644 --- a/packages/repository/test/unit/decorator/repository-with-di.ts +++ b/packages/repository/test/unit/decorator/repository-with-di.ts @@ -64,7 +64,7 @@ describe('repository class', () => { // tslint:disable-next-line:max-line-length it('supports referencing predefined repository by name via constructor', async () => { - const myController: MyController = await ctx.get( + const myController = await ctx.get( 'controllers.MyController', ); expect(myController.noteRepo instanceof DefaultCrudRepository).to.be.true(); diff --git a/packages/repository/test/unit/decorator/repository-with-value-provider.ts b/packages/repository/test/unit/decorator/repository-with-value-provider.ts index 75e356c93be4..0e7e2f466af0 100644 --- a/packages/repository/test/unit/decorator/repository-with-value-provider.ts +++ b/packages/repository/test/unit/decorator/repository-with-value-provider.ts @@ -62,7 +62,7 @@ describe('repository class', () => { // tslint:disable-next-line:max-line-length it('supports referencing predefined repository by name via constructor', async () => { - const myController: MyController = await ctx.get( + const myController = await ctx.get( 'controllers.MyController', ); expect(myController.noteRepo instanceof DefaultCrudRepository).to.be.true(); diff --git a/packages/repository/test/unit/decorator/repository.ts b/packages/repository/test/unit/decorator/repository.ts index 977d26fcff90..edd3e264b61d 100644 --- a/packages/repository/test/unit/decorator/repository.ts +++ b/packages/repository/test/unit/decorator/repository.ts @@ -54,7 +54,7 @@ describe('repository decorator', () => { // tslint:disable-next-line:max-line-length it('supports referencing predefined repository by name via constructor', async () => { - const myController: MyController = await ctx.get( + const myController = await ctx.get( 'controllers.MyController', ); expect(myController.noteRepo).exactly(repo); @@ -62,7 +62,7 @@ describe('repository decorator', () => { // tslint:disable-next-line:max-line-length it('supports referencing predefined repository by name via property', async () => { - const myController: MyController = await ctx.get( + const myController = await ctx.get( 'controllers.MyController', ); expect(myController.noteRepo2).exactly(repo); @@ -84,7 +84,8 @@ describe('repository decorator', () => { ) {} } ctx.bind('controllers.Controller2').toClass(Controller2); - const myController: MyController = await ctx.get('controllers.Controller2'); + + const myController = await ctx.get('controllers.Controller2'); expect(myController.noteRepo).to.be.not.null(); }); @@ -96,7 +97,7 @@ describe('repository decorator', () => { ) {} } ctx.bind('controllers.Controller3').toClass(Controller3); - const myController: MyController = await ctx.get('controllers.Controller3'); + const myController = await ctx.get('controllers.Controller3'); expect(myController.noteRepo).to.be.not.null(); }); diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index 700e1ca3f7f7..f87087cd33c7 100644 --- a/packages/rest/src/http-handler.ts +++ b/packages/rest/src/http-handler.ts @@ -64,7 +64,7 @@ export class HttpHandler { const parsedRequest: ParsedRequest = parseRequestUrl(request); const requestContext = this._createRequestContext(request, response); - const sequence: SequenceHandler = await requestContext.get( + const sequence = await requestContext.get( RestBindings.SEQUENCE, ); await sequence.handle(parsedRequest, response); diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index c98f0a06a864..bf808cc0f596 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -220,7 +220,7 @@ export class RestServer extends Context implements Server { for (const b of this.find('routes.*')) { // TODO(bajtos) should we support routes defined asynchronously? - const route = this.getSync(b.key); + const route = this.getSync(b.key); this._httpHandler.registerRoute(route); } @@ -445,7 +445,7 @@ export class RestServer extends Context implements Server { * - `app.route('get', '/greet', operationSpec, MyController, 'greet')` */ getApiSpec(): OpenApiSpec { - const spec = this.getSync(RestBindings.API_SPEC); + const spec = this.getSync(RestBindings.API_SPEC); const defs = this.httpHandler.getApiDefinitions(); // Apply deep clone to prevent getApiSpec() callers from @@ -527,8 +527,8 @@ export class RestServer extends Context implements Server { // of API spec, controllers and routes at startup time. this._setupHandlerIfNeeded(); - const httpPort = await this.get(RestBindings.PORT); - const httpHost = await this.get(RestBindings.HOST); + const httpPort = await this.get(RestBindings.PORT); + const httpHost = await this.get(RestBindings.HOST); this._httpServer = createServer(this.handleHttp); const httpServer = this._httpServer; diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index c665efd8f0ea..c99e5cb5a782 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -238,7 +238,7 @@ describe('Routing', () => { } async getFlag(): Promise { - return this.ctx.get('flag'); + return this.ctx.get('flag'); } } givenControllerInApp(app, FlagController); diff --git a/packages/rest/test/unit/rest-component.test.ts b/packages/rest/test/unit/rest-component.test.ts index 97d1c22523df..3ea6e80d4260 100644 --- a/packages/rest/test/unit/rest-component.test.ts +++ b/packages/rest/test/unit/rest-component.test.ts @@ -4,7 +4,12 @@ // License text available at https://opensource.org/licenses/MIT import {expect, ShotRequest} from '@loopback/testlab'; -import {Application, ProviderMap, CoreBindings} from '@loopback/core'; +import { + Application, + ProviderMap, + CoreBindings, + Component, +} from '@loopback/core'; import {inject, Provider, BoundValue, Context} from '@loopback/context'; import { @@ -14,12 +19,13 @@ import { RestComponentConfig, ServerRequest, HttpHandler, + LogError, } from '../..'; const SequenceActions = RestBindings.SequenceActions; describe('RestComponent', () => { describe('Providers', () => { - describe('Default implementations are bound', () => { + describe('Default implementations are bound', async () => { const app = new Application(); app.component(RestComponent); @@ -27,12 +33,11 @@ describe('RestComponent', () => { app.bind(RestBindings.Http.CONTEXT).to(new Context()); app.bind(RestBindings.HANDLER).to(new HttpHandler(app)); - // Mocha can't dynamically generate the tests if we use app.get(...) - const comp = app.getSync('components.RestComponent'); - for (const key in comp.providers) { + const comp = await app.get('components.RestComponent'); + for (const key in comp.providers || {}) { it(key, async () => { const result = await app.get(key); - const expected: Provider = new comp.providers[key](); + const expected: Provider = new comp.providers![key](); expect(result).to.deepEqual(expected.value()); }); } @@ -42,7 +47,7 @@ describe('RestComponent', () => { const app = new Application(); app.component(RestComponent); const server = await app.getServer(RestServer); - const logError = await server.get(SequenceActions.LOG_ERROR); + const logError = await server.get(SequenceActions.LOG_ERROR); expect(logError.length).to.equal(3); // (err, statusCode, request) }); @@ -74,7 +79,7 @@ describe('RestComponent', () => { const app = new Application(); app.component(CustomRestComponent); const server = await app.getServer(RestServer); - const logError = await server.get(SequenceActions.LOG_ERROR); + const logError = await server.get(SequenceActions.LOG_ERROR); logError(new Error('test-error'), 400, new ShotRequest({url: '/'})); expect(lastLog).to.equal('/ 400 test-error'); diff --git a/packages/rest/test/unit/rest-server/rest-server.test.ts b/packages/rest/test/unit/rest-server/rest-server.test.ts index 1430b4d86d84..9f8b6416f6a5 100644 --- a/packages/rest/test/unit/rest-server/rest-server.test.ts +++ b/packages/rest/test/unit/rest-server/rest-server.test.ts @@ -21,7 +21,7 @@ describe('RestServer', () => { describe('"bindElement" binding', () => { it('returns a function for creating new bindings', async () => { const ctx = await givenRequestContext(); - const bindElement: BindElement = await ctx.get(RestBindings.BIND_ELEMENT); + const bindElement = await ctx.get(RestBindings.BIND_ELEMENT); const binding = bindElement('foo').to('bar'); expect(binding).to.be.instanceOf(Binding); expect(ctx.getSync('foo')).to.equal('bar'); @@ -31,7 +31,7 @@ describe('RestServer', () => { describe('"getFromContext" binding', () => { it('returns a function for getting a value from the context', async () => { const ctx = await givenRequestContext(); - const getFromContext: GetFromContext = await ctx.get( + const getFromContext = await ctx.get( RestBindings.GET_FROM_CONTEXT, ); ctx.bind('foo').to('bar'); @@ -53,7 +53,7 @@ describe('RestServer', () => { ); const ctx = await givenRequestContext(); - const invokeMethod: InvokeMethod = await ctx.get( + const invokeMethod = await ctx.get( RestBindings.SequenceActions.INVOKE_METHOD, );