diff --git a/packages/authentication/test/acceptance/authentication/feature.md b/packages/authentication/test/acceptance/authentication/feature.md index 6dbdff01864d..b6273107c595 100644 --- a/packages/authentication/test/acceptance/authentication/feature.md +++ b/packages/authentication/test/acceptance/authentication/feature.md @@ -58,10 +58,10 @@ const USERS = { }; // my get user function -app.bind('authentication.user').to(() => { +app.bind('authentication.user').to(async () => { const ctx = this; - const username = ctx.get('authentication.credentials.username'); - const password = ctx.get('authentication.credentials.password'); + const username = await ctx.get('authentication.credentials.username'); + const password = await ctx.get('authentication.credentials.password'); const user = USERS[username]; if (!user) return null; if (!verifyPassword(user.password, password)) return null; diff --git a/packages/context/index.ts b/packages/context/index.ts index f17c90dd5fad..56be30b19746 100644 --- a/packages/context/index.ts +++ b/packages/context/index.ts @@ -4,5 +4,7 @@ // License text available at https://opensource.org/licenses/MIT export {Binding, BoundValue} from './src/binding'; -export {Context, Constructor} from './src/context'; +export {Context} from './src/context'; +export {Constructor} from './src/resolver'; export {inject} from './src/inject'; +export const isPromise = require('is-promise'); diff --git a/packages/context/package.json b/packages/context/package.json index 9f2aaa3c5fb6..8c6fc7f4ce5f 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -16,6 +16,8 @@ }, "devDependencies": { "@loopback/testlab": "^4.0.0-alpha.1", + "@types/bluebird": "^3.5.2", + "bluebird": "^3.5.0", "mocha": "^3.2.0" }, "keywords": [ diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index 4f0275431f17..4235eafd5b8d 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -3,15 +3,14 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context, Constructor} from './context'; +import {Context} from './context'; +import {Constructor, instantiateClass} from './resolver'; // tslint:disable-next-line:no-any export type BoundValue = any; +// FIXME(bajtos) The binding class should be parameterized by the value type stored export class Binding { - // FIXME(bajtos) The binding class should be parameterized by the value type stored - public value: BoundValue; - public getValue: () => BoundValue = () => { throw new Error(`No value was configured for binding ${this._key}.`); }; private _tagName: string; // For bindings bound via toClass, this property contains the constructor function @@ -21,6 +20,30 @@ export class Binding { get key() { return this._key; } get tagName() { return this._tagName; } + /** + * This is an internal function optimized for performance. + * Users should use `@inject(key)` or `ctx.get(key)` instead. + * + * Get the value bound to this key. Depending on `isSync`, this function returns either: + * - the bound value + * - a promise of the bound value + * + * Consumers wishing to consume sync values directly should use `isPromise` + * to check the type of the returned value to decide how to handle it. + * + * ``` + * const result = binding.getValue(); + * if (isPromise(result)) { + * result.then(doSomething) + * } else { + * doSomething(result); + * } + * ``` + */ + getValue(): BoundValue | Promise { + return Promise.reject(new Error(`No value was configured for binding ${this._key}.`)); + } + lock() { this.isLocked = true; } @@ -30,18 +53,54 @@ export class Binding { return this; } + /** + * Bind the key to a constant value. + * + * @param value The bound value. + * + * @example + * + * ```ts + * ctx.bind('appName').to('CodeHub'); + * ``` + */ to(value: BoundValue): this { this.getValue = () => value; return this; } - toDynamicValue(factoryFn: () => BoundValue): this { + /** + * Bind the key to a computed (dynamic) value. + * + * @param factoryFn The factory function creating the value. + * Both sync and async functions are supported. + * + * @example + * + * ```ts + * // synchronous + * ctx.bind('now').toDynamicValue(() => Date.now()); + * + * // asynchronous + * ctx.bind('something').toDynamicValue( + * async () => Promise.delay(10).then(doSomething) + * ); + * ``` + */ + toDynamicValue(factoryFn: () => BoundValue | Promise): this { this.getValue = factoryFn; return this; } + /** + * Bind the key to an instance of the given class. + * + * @param ctor The class constructor to call. Any constructor + * arguments must be annotated with `@inject` so that + * we can resolve them from the context. + */ toClass(ctor: Constructor): this { - this.getValue = () => this._context.createClassInstance(ctor); + this.getValue = () => instantiateClass(ctor, this._context); this.valueConstructor = ctor; return this; } diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index e662ea677d16..f1e8cdc98c70 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -4,10 +4,8 @@ // License text available at https://opensource.org/licenses/MIT import {Binding, BoundValue} from './binding'; -import {inject, describeInjectedArguments} from './inject'; - -// tslint:disable-next-line:no-any -export type Constructor = new(...args: any[]) => T; +import {inject} from './inject'; +import {isPromise} from './isPromise'; export class Context { private registry: Map; @@ -30,32 +28,6 @@ export class Context { return binding; } - createClassInstance(ctor: Constructor) : T { - const args = this._resolveInjectedArguments(ctor); - return new ctor(...args); - } - - private _resolveInjectedArguments(fn: Function): BoundValue[] { - const args: BoundValue[] = []; - // NOTE: the array may be sparse, i.e. - // Object.keys(injectedArgs).length !== injectedArgs.length - // Example value: - // [ , 'key1', , 'key2'] - const injectedArgs = describeInjectedArguments(fn); - - for (let ix = 0; ix < fn.length; ix++) { - const bindingKey = injectedArgs[ix]; - if (!bindingKey) { - throw new Error( - `Cannot resolve injected arguments for function ${fn.name}: ` + - `The argument ${ix+1} was not decorated for dependency injection.`); - } - - args.push(this.get(bindingKey)); - } - return args; - } - contains(key: string): boolean { return this.registry.has(key); } @@ -89,10 +61,32 @@ export class Context { return bindings; } - get(key: string) { + get(key: string): Promise { + try { + const binding = this.getBinding(key); + return Promise.resolve(binding.getValue()); + } catch (err) { + return Promise.reject(err); + } + } + + getSync(key: string): BoundValue { + const binding = this.getBinding(key); + const valueOrPromise = binding.getValue(); + + if (isPromise(valueOrPromise)) { + throw new Error( + `Cannot get ${key} synchronously: ` + + `the value requires async computation`); + } + + return valueOrPromise; + } + + getBinding(key: string): Binding { const binding = this.registry.get(key); if (!binding) throw new Error(`The key ${key} was not bound to any value.`); - return binding.getValue(); + return binding; } } diff --git a/packages/context/src/isPromise.ts b/packages/context/src/isPromise.ts new file mode 100644 index 000000000000..2b1d3bba1a8c --- /dev/null +++ b/packages/context/src/isPromise.ts @@ -0,0 +1,12 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export function isPromise(value: T | Promise): value is Promise { + if (!value) + return false; + if (typeof value !== 'object' && typeof value !== 'function') + return false; + return typeof (value as Promise).then === 'function'; +} diff --git a/packages/context/src/resolver.ts b/packages/context/src/resolver.ts new file mode 100644 index 000000000000..e388b7549cbf --- /dev/null +++ b/packages/context/src/resolver.ts @@ -0,0 +1,77 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { Context } from './context'; +import { Binding, BoundValue } from './binding'; +import { isPromise } from './isPromise'; +import { describeInjectedArguments } from './inject'; + +// tslint:disable-next-line:no-any +export type Constructor = new(...args: any[]) => T; + +/** + * Create an instance of a class which constructor has arguments + * decorated with `@inject`. + * + * The function returns a class when all dependencies were + * resolved synchronously, or a Promise otherwise. + * + * @param ctor The class constructor to call. + * @param ctx The context containing values for `@inject` resolution + */ +export function instantiateClass(ctor: Constructor, ctx: Context): T | Promise { + const argsOrPromise = resolveInjectedArguments(ctor, ctx); + if (isPromise(argsOrPromise)) { + return argsOrPromise.then(args => new ctor(...args)); + } else { + return new ctor(...argsOrPromise); + } +} + +/** + * Given a function with arguments decorated with `@inject`, + * return the list of arguments resolved using the values + * bound in `ctx`. + + * The function returns an argument array when all dependencies were + * resolved synchronously, or a Promise otherwise. + * + * @param fn The function for which the arguments should be resolved. + * @param ctx The context containing values for `@inject` resolution + */ +export function resolveInjectedArguments(fn: Function, ctx: Context): BoundValue[] | Promise { + // NOTE: the array may be sparse, i.e. + // Object.keys(injectedArgs).length !== injectedArgs.length + // Example value: + // [ , 'key1', , 'key2'] + const injectedArgs = describeInjectedArguments(fn); + + const args: BoundValue[] = new Array(fn.length); + let asyncResolvers: Promise[] | undefined = undefined; + + for (let ix = 0; ix < fn.length; ix++) { + const bindingKey = injectedArgs[ix]; + if (!bindingKey) { + throw new Error( + `Cannot resolve injected arguments for function ${fn.name}: ` + + `The argument ${ix + 1} was not decorated for dependency injection.`); + } + + const binding = ctx.getBinding(bindingKey); + const valueOrPromise = binding.getValue(); + if (isPromise(valueOrPromise)) { + if (!asyncResolvers) asyncResolvers = []; + asyncResolvers.push(valueOrPromise.then((v: BoundValue) => args[ix] = v)); + } else { + args[ix] = valueOrPromise as BoundValue; + } + } + + if (asyncResolvers) { + return Promise.all(asyncResolvers).then(() => args); + } else { + return args; + } +} diff --git a/packages/context/test/acceptance/_feature.md b/packages/context/test/acceptance/_feature.md index ced2aba1d41a..1cfc97ea5572 100644 --- a/packages/context/test/acceptance/_feature.md +++ b/packages/context/test/acceptance/_feature.md @@ -26,8 +26,8 @@ child.get('foo'); // => 'bar' let ctx = new Context(); ctx.bind(':name').to('hello world') -ctx.get('foo'); // => hello world -ctx.get('bat'); // => hello world +await ctx.get('foo'); // => hello world +await ctx.get('bat'); // => hello world ``` ## Scenario: Simple Dynamic Paramaterized Binding @@ -47,8 +47,8 @@ ctx.bind(':name').to((name) => { return data[name]; }); -ctx.get('foo'); // => bar -ctx.get('bat'); // => baz +await ctx.get('foo'); // => bar +await ctx.get('bat'); // => baz ``` ## Scenario: Namespaced Paramaterized Binding @@ -61,6 +61,6 @@ ctx.get('bat'); // => baz let ctx = new Context(); ctx.bind('foo.:name').to('hello world'); -ctx.get('foo.bar'); // => hello world -ctx.get('foo.bat'); // => hello world +await ctx.get('foo.bar'); // => hello world +await ctx.get('foo.bat'); // => hello world ``` diff --git a/packages/context/test/acceptance/class-level-bindings.feature.md b/packages/context/test/acceptance/class-level-bindings.feature.md index e1736989d3d0..fb473994cb37 100644 --- a/packages/context/test/acceptance/class-level-bindings.feature.md +++ b/packages/context/test/acceptance/class-level-bindings.feature.md @@ -28,6 +28,6 @@ } ctx.bind('controllers.info').toClass(InfoController); - const instance = ctx.get('controllers.info'); + const instance = await ctx.get('controllers.info'); instance.appName; // => CodeHub ``` diff --git a/packages/context/test/acceptance/class-level-bindings.ts b/packages/context/test/acceptance/class-level-bindings.ts index ad2d3fbfa929..9932377adf6d 100644 --- a/packages/context/test/acceptance/class-level-bindings.ts +++ b/packages/context/test/acceptance/class-level-bindings.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from 'testlab'; -import {Context, inject} from '../..'; +import {Context, inject, isPromise} from '../..'; const INFO_CONTROLLER = 'controllers.info'; @@ -12,7 +12,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { let ctx: Context; before('given a context', createContext); - it('injects constructor args', () => { + it('injects constructor args', async () => { ctx.bind('application.name').to('CodeHub'); class InfoController { @@ -21,7 +21,7 @@ describe('Context bindings - Injecting dependencies of classes', () => { } ctx.bind(INFO_CONTROLLER).toClass(InfoController); - const instance = ctx.get(INFO_CONTROLLER); + const instance = await ctx.get(INFO_CONTROLLER); expect(instance).to.have.property('appName', 'CodeHub'); }); @@ -32,9 +32,9 @@ describe('Context bindings - Injecting dependencies of classes', () => { } ctx.bind(INFO_CONTROLLER).toClass(InfoController); - expect.throws( - () => ctx.get(INFO_CONTROLLER), - /resolve.*InfoController.*argument 1/); + return ctx.get(INFO_CONTROLLER).then( + function onSuccess() { throw new Error('ctx.get() should have failed'); }, + function onError(err) { expect(err).to.match(/resolve.*InfoController.*argument 1/); }); }); it('throws helpful error when some ctor args are not decorated', () => { @@ -46,9 +46,40 @@ describe('Context bindings - Injecting dependencies of classes', () => { } ctx.bind(INFO_CONTROLLER).toClass(InfoController); - expect.throws( - () => ctx.get(INFO_CONTROLLER), - /resolve.*InfoController.*argument 1/); + return ctx.get(INFO_CONTROLLER).then( + function onSuccess() { throw new Error('ctx.get() should have failed'); }, + function onError(err) { expect(err).to.match(/resolve.*InfoController.*argument 1/); }); + }); + + it('resolves promises before injecting parameters', async () => { + ctx.bind('authenticated').toDynamicValue(async () => { + // Emulate asynchronous database call + await Promise.resolve(); + // Return the authentication result + return false; + }); + + class InfoController { + constructor(@inject('authenticated') public isAuthenticated: boolean) { + } + } + ctx.bind(INFO_CONTROLLER).toClass(InfoController); + + const instance = await ctx.get(INFO_CONTROLLER); + expect(instance).to.have.property('isAuthenticated', false); + }); + + it('creates instance synchronously when all dependencies are sync too', () => { + ctx.bind('appName').to('CodeHub'); + class InfoController { + constructor(@inject('appName') public appName: string) { + } + } + const b = ctx.bind(INFO_CONTROLLER).toClass(InfoController); + + const valueOrPromise = b.getValue(); + expect(valueOrPromise).to.not.be.Promise(); + expect(valueOrPromise as InfoController).to.have.property('appName', 'CodeHub'); }); function createContext() { diff --git a/packages/context/test/acceptance/creating-and-resolving-bindings.feature.md b/packages/context/test/acceptance/creating-and-resolving-bindings.feature.md index a2d877a55302..324aaa79485f 100644 --- a/packages/context/test/acceptance/creating-and-resolving-bindings.feature.md +++ b/packages/context/test/acceptance/creating-and-resolving-bindings.feature.md @@ -22,7 +22,7 @@ ctx.bind('foo').to('bar'); ctx.contains('foo'); // true // ensure bound to value `bar` is returned -const val = ctx.get('foo'); // val => bar +const val = await ctx.get('foo'); // val => bar ``` ## Scenario: Dynamic Bindings @@ -44,7 +44,7 @@ ctx.bind('data').toDynamicValue(() => { return data.shift(); }); -ctx.get('data'); // => a -ctx.get('data'); // => b -ctx.get('data'); // => c +await ctx.get('data'); // => a +await ctx.get('data'); // => b +await ctx.get('data'); // => c ``` diff --git a/packages/context/test/acceptance/creating-and-resolving-bindings.ts b/packages/context/test/acceptance/creating-and-resolving-bindings.ts index d5d2aeab2ece..6a4d46718954 100644 --- a/packages/context/test/acceptance/creating-and-resolving-bindings.ts +++ b/packages/context/test/acceptance/creating-and-resolving-bindings.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from 'testlab'; -import {Context} from '../..'; +import {Context, isPromise} from '../..'; describe('Context bindings - Creating and resolving bindings', () => { let ctx: Context; @@ -19,16 +19,19 @@ describe('Context bindings - Creating and resolving bindings', () => { expect(ctx.contains('foo')).to.be.true(); }); - function createBinding() { - ctx.bind('foo').to('bar'); - } - }); + it('returns the bound value `bar`', async () => { + const result = await ctx.get('foo'); + expect(result).to.equal('bar'); + }); - context('is resolved', () => { - it('returns the bound value `bar`', () => { - const result = ctx.get('foo'); + it('supports sync retrieval of the bound value', () => { + const result = ctx.getSync('foo'); expect(result).to.equal('bar'); }); + + function createBinding() { + ctx.bind('foo').to('bar'); + } }); }); }); @@ -38,33 +41,39 @@ describe('Context bindings - Creating and resolving bindings', () => { before(createDynamicBinding); context('resolving the binding for the first time', () => { - it('returns the first value', () => { - const result = ctx.get('data'); + it('returns the first value', async () => { + const result = await ctx.get('data'); expect(result).to.equal('a'); }); }); context('resolving the binding for the second time', () => { - it('returns the second value', () => { - const result = ctx.get('data'); + it('returns the second value', async () => { + const result = await ctx.get('data'); expect(result).to.equal('b'); }); }); context('resolving the binding for the third time', () => { - it('returns the third value', () => { - const result = ctx.get('data'); + it('returns the third value', async () => { + const result = await ctx.get('data'); expect(result).to.equal('c'); }); }); + + function createDynamicBinding() { + const data = ['a', 'b', 'c']; + ctx.bind('data').toDynamicValue(function() { + return data.shift(); + }); + } }); - function createDynamicBinding() { - const data = ['a', 'b', 'c']; - ctx.bind('data').toDynamicValue(function() { - return data.shift(); - }); - } + it('can resolve synchronously when the factory function is sync', () => { + ctx.bind('data').toDynamicValue(() => 'value'); + const result = ctx.getSync('data'); + expect(result).to.equal('value'); + }); }); function createContext() { diff --git a/packages/context/test/acceptance/unlocking-bindings.feature.md b/packages/context/test/acceptance/unlocking-bindings.feature.md index 88539be32ab0..9e6c58a0d801 100644 --- a/packages/context/test/acceptance/unlocking-bindings.feature.md +++ b/packages/context/test/acceptance/unlocking-bindings.feature.md @@ -30,5 +30,5 @@ binding.unlock(); ctx.bind('foo').to('baz'); // new value is baz -console.log(ctx.get('foo')); // => baz +console.log(await ctx.get('foo')); // => baz ``` diff --git a/packages/context/test/acceptance/unlocking-bindings.ts b/packages/context/test/acceptance/unlocking-bindings.ts index e1a972819fa0..af0ec5580674 100644 --- a/packages/context/test/acceptance/unlocking-bindings.ts +++ b/packages/context/test/acceptance/unlocking-bindings.ts @@ -35,8 +35,8 @@ describe(`Context bindings - Unlocking bindings`, () => { expect(operation).to.not.throw(); }); - it('binds the duplicate key to the new value', () => { - const result = ctx.get('foo'); + it('binds the duplicate key to the new value', async () => { + const result = await ctx.get('foo'); expect(result).to.equal('baz'); }); }); diff --git a/packages/context/test/unit/binding.ts b/packages/context/test/unit/binding.ts index 68eaacc09b82..22a616b1d931 100644 --- a/packages/context/test/unit/binding.ts +++ b/packages/context/test/unit/binding.ts @@ -30,6 +30,13 @@ describe('Binding', () => { }); }); + describe('to(value)', () => { + it('returns the value synchronously', () => { + binding.to('value'); + expect(binding.getValue()).to.equal('value'); + }); + }); + function givenBinding() { const ctx = new Context(); binding = new Binding(ctx, key); diff --git a/packages/context/test/unit/context.ts b/packages/context/test/unit/context.ts index f3587c332c61..122e5e6ea0fe 100644 --- a/packages/context/test/unit/context.ts +++ b/packages/context/test/unit/context.ts @@ -41,6 +41,31 @@ describe('Context', () => { }); }); + describe('getBinding', () => { + it('returns the binding object registered under the given key', () => { + const expected = ctx.bind('foo'); + const actual = ctx.getBinding('foo'); + expect(actual).to.equal(expected); + }); + + it('reports an error when binding was not found', () => { + expect(() => ctx.getBinding('unknown-key')).to.throw(/unknown-key/); + }); + }); + + describe('getSync', () => { + it('returns the value immediately when the binding is sync', () => { + ctx.bind('foo').to('bar'); + const result = ctx.getSync('foo'); + expect(result).to.equal('bar'); + }); + + it('throws a helpful error when the binding is async', () => { + ctx.bind('foo').toDynamicValue(() => Promise.resolve('bar')); + expect(() => ctx.getSync('foo')).to.throw(/foo.*async/); + }); + }); + function createContext() { ctx = new Context(); } diff --git a/packages/context/test/unit/isPromise.test.ts b/packages/context/test/unit/isPromise.test.ts new file mode 100644 index 000000000000..92b011cc84ea --- /dev/null +++ b/packages/context/test/unit/isPromise.test.ts @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2013,2017. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as bluebird from 'bluebird'; +import {expect} from '@loopback/testlab'; +import {isPromise} from '../..'; + +describe('isPromise', () => { + it('returns false for undefined', () => { + expect(isPromise(undefined)).to.be.false(); + }); + + it('returns false for a string value', () => { + expect(isPromise('string-value')).to.be.false(); + }); + + it('returns false for a plain object', () => { + expect(isPromise({foo: 'bar'})).to.be.false(); + }); + + it('returns false for an array', () => { + expect(isPromise([1, 2, 3])).to.be.false(); + }); + + it('returns false for a Date', () => { + expect(isPromise(new Date())).to.be.false(); + }); + + it('returns true for a native Promise', () => { + expect(isPromise(Promise.resolve())).to.be.true(); + }); + + it('returns true for a Bluebird Promise', () => { + expect(isPromise(bluebird.resolve())).to.be.true(); + }); + + it('returns false when .then() is not a function', () => { + expect(isPromise({ then: 'later' })).to.be.false(); + }); +}); diff --git a/packages/example-codehub/src/CodeHubApplication.ts b/packages/example-codehub/src/CodeHubApplication.ts index 76fe5e596854..08a61b50dca7 100644 --- a/packages/example-codehub/src/CodeHubApplication.ts +++ b/packages/example-codehub/src/CodeHubApplication.ts @@ -27,14 +27,15 @@ export class CodeHubApplication extends Application { async start() { this._startTime = new Date(); - const server = new Server({port: this.get('servers.http.port')}); + const httpPort = await this.get('servers.http.port'); + const server = new Server({port: httpPort}); this.bind('servers.http.server').to(server); server.bind('applications.code-hub').to(this); return server.start(); } - info() { - const server = this.get('servers.http.server') as Server; + async info() { + const server = await this.get('servers.http.server') as Server; const port = server.config.port; return { diff --git a/packages/example-codehub/test/support/util.ts b/packages/example-codehub/test/support/util.ts index 65ca80c73d71..e146924b47e6 100644 --- a/packages/example-codehub/test/support/util.ts +++ b/packages/example-codehub/test/support/util.ts @@ -6,8 +6,8 @@ import {supertest} from 'testlab'; import {CodeHubApplication} from 'example-codehub/src/CodeHubApplication'; -export function createClientForApp(app: CodeHubApplication) { - const url = app.info().url; +export async function createClientForApp(app: CodeHubApplication) { + const url = (await app.info()).url; return supertest(url); } @@ -20,6 +20,6 @@ export function createApp() { export async function createAppAndClient() { const app = createApp(); await app.start(); - const client = createClientForApp(app); + const client = await createClientForApp(app); return {app, client}; } diff --git a/packages/loopback/lib/router/SwaggerRouter.ts b/packages/loopback/lib/router/SwaggerRouter.ts index b37c97b1e1db..4de78211032c 100644 --- a/packages/loopback/lib/router/SwaggerRouter.ts +++ b/packages/loopback/lib/router/SwaggerRouter.ts @@ -183,19 +183,20 @@ class Endpoint { pathParams[key.name] = match[matchIndex]; } - const controller = this._controllerFactory(request, response); const operationName = this._spec['x-operation-name']; - - loadRequestBodyIfNeeded(this._spec, request) - .then(body => buildOperationArguments(this._spec, request, pathParams, body)) - .then( - args => { - this._invoke(controller, operationName, args, response, next); - }, - err => { - debug('Cannot parse arguments of operation %s: %s', operationName, err.stack || err); - next(err); - }); + Promise.resolve(this._controllerFactory(request, response)) + .then(controller => { + loadRequestBodyIfNeeded(this._spec, request) + .then(body => buildOperationArguments(this._spec, request, pathParams, body)) + .then( + args => { + this._invoke(controller, operationName, args, response, next); + }, + err => { + debug('Cannot parse arguments of operation %s: %s', operationName, err.stack || err); + next(err); + }); + }); } private _invoke(controller: Object, operationName: string, args: OperationArgs, response: Response, next: HandlerCallback) { diff --git a/packages/loopback/lib/server.ts b/packages/loopback/lib/server.ts index 69d8d8a521f1..f259d947b30d 100644 --- a/packages/loopback/lib/server.ts +++ b/packages/loopback/lib/server.ts @@ -40,11 +40,13 @@ export class Server extends Context { // after the app started. The idea is to rebuild the SwaggerRouter // instance whenever a controller was added/deleted. const router = new SwaggerRouter(); - this.find('applications.*').forEach(appBinding => { + + const apps = this.find('applications.*'); + for (const appBinding of apps) { debug('Registering app controllers for %j', appBinding.key); - const app = appBinding.getValue() as Application; + const app = await appBinding.getValue() as Application; app.mountControllers(router); - }); + } const server = http.createServer(router.handler); diff --git a/packages/loopback/test/acceptance/routing/feature.md b/packages/loopback/test/acceptance/routing/feature.md index 317507fdff74..4a83783bbfac 100644 --- a/packages/loopback/test/acceptance/routing/feature.md +++ b/packages/loopback/test/acceptance/routing/feature.md @@ -102,7 +102,7 @@ app.bind('currentMethod').toDynamicValue(() => { }); -server.on('request', async (req, res) { +server.on('request', async (req, res) => { let ctx = new Context(); ctx.bind('url').to(req.url); ctx.bind('req.body').toPromise((reject, resolve) => { @@ -110,15 +110,15 @@ server.on('request', async (req, res) { }); ctx.bind('req').to(req); - let controller = ctx.get('currentController'); + let controller = await ctx.get('currentController'); // allow apps to create / customize bindings controller.bind(); ctx.bind('result') - .toPromise((reject, resolve, ctx) => { - let method = ctx.get('currentMethod'); - let methodArgs = ctx.get('currentArgs'); + .toPromise(async (reject, resolve, ctx) => { + let method = await ctx.get('currentMethod'); + let methodArgs = await ctx.get('currentArgs'); return method.invoke(args); }) .memoize()