From fb01931d4e193c21560811f4d6d078c89941fcfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 24 Aug 2018 14:47:11 +0200 Subject: [PATCH] feat(service-proxy): add service mixin Implement "ServiceMixin" for applications. This mixin enhances component registration so that service providers exported by a component are automatically registered for dependency injection; and adds a new sugar API for registering service providers manually: app.serviceProvider(MyServiceProvicer); The method name "serviceProvider" was chosen deliberately to make it clear that we are binding a Provider, not a class constructor. Compare this to `app.repository(MyRepo)` that accepts a class construct. In the future, we may add `app.service(MyService)` method if there is enough user demand. --- .../Calling-other-APIs-and-Web-Services.md | 21 +- ...ap-calculator-tutorial-register-service.md | 49 ++--- docs/site/todo-tutorial-geocoding-service.md | 11 +- examples/soap-calculator/src/application.ts | 12 +- examples/todo/src/application.ts | 15 +- .../boot/src/booters/datasource.booter.ts | 9 +- .../boot/src/booters/repository.booter.ts | 5 +- .../unit/booters/datasource.booter.unit.ts | 7 +- .../unit/booters/repository.booter.unit.ts | 7 +- packages/cli/generators/app/index.js | 22 ++ .../app/templates/src/application.ts.ejs | 12 +- .../project/templates/package.json.ejs | 9 +- .../integration/generators/app.integration.js | 2 +- .../repository/src/mixins/repository.mixin.ts | 8 +- .../has-many.relation.acceptance.ts | 4 +- packages/service-proxy/docs.json | 1 + packages/service-proxy/src/index.ts | 1 + packages/service-proxy/src/mixins/index.ts | 6 + .../service-proxy/src/mixins/service.mixin.ts | 198 ++++++++++++++++++ .../test/unit/mixin/service.mixin.unit.ts | 97 +++++++++ 20 files changed, 407 insertions(+), 89 deletions(-) create mode 100644 packages/service-proxy/src/mixins/index.ts create mode 100644 packages/service-proxy/src/mixins/service.mixin.ts create mode 100644 packages/service-proxy/test/unit/mixin/service.mixin.unit.ts diff --git a/docs/site/Calling-other-APIs-and-Web-Services.md b/docs/site/Calling-other-APIs-and-Web-Services.md index 7787d84b36b9..e8da9d5e84dc 100644 --- a/docs/site/Calling-other-APIs-and-Web-Services.md +++ b/docs/site/Calling-other-APIs-and-Web-Services.md @@ -60,19 +60,6 @@ Install the REST connector used by the new datasource: $ npm install --save loopback-connector-rest ``` -### Bind data sources to the context - -```ts -import {Context} from '@loopback/context'; - -const context = new Context(); -context.bind('dataSources.geoService').to(ds); -``` - -**NOTE**: Once we start to support declarative datasources with -`@loopback/boot`, the datasource configuration files can be dropped into -`src/datasources` to be discovered and bound automatically. - ### Declare the service interface To promote type safety, we recommend you to declare data types and service @@ -162,10 +149,12 @@ export class GeoServiceProvider implements Provider { } ``` -In your application setup, create an explicit binding for the geo service proxy: +In your application, apply +[ServiceMixin](http://apidocs.loopback.io/@loopback%2fdocs/service-proxy.html#ServiceMixin) +and use `app.serviceProvider` API to create binding for the geo service proxy. ```ts -app.bind('services.geo').toProvider(GeoServiceProvider); +app.serviceProvider(GeoServiceProvider); ``` Finally, modify the controller to receive our new service proxy in the @@ -173,7 +162,7 @@ constructor: ```ts export class MyController { - @inject('services.geo') + @inject('services.GeoService') private geoService: GeoService; } ``` diff --git a/docs/site/soap-calculator-tutorial-register-service.md b/docs/site/soap-calculator-tutorial-register-service.md index 2fb97a8e2d20..c41944becd12 100644 --- a/docs/site/soap-calculator-tutorial-register-service.md +++ b/docs/site/soap-calculator-tutorial-register-service.md @@ -15,33 +15,33 @@ Injection)_. #### Importing the service and helper classes -Add the following import statement after all the previous imports. +Add the following import statements after all the previous imports. ```ts +import {ServiceMixin} from '@loopback/service-proxy'; import {CalculatorServiceProvider} from './services/calculator.service'; ``` -Now change the following line to include a Constructor and Provider class from -_LB4_ core. +#### Applying `ServiceMixin` on our Application class -```ts -import {ApplicationConfig} from '@loopback/core'; -``` - -change it to +Modify the inheritance chain of our Application class as follows: ```ts -import {ApplicationConfig, Constructor, Provider} from '@loopback/core'; +export class SoapCalculatorApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + // (no changes in application constructor or methods) +} ``` #### Registering the Service and bind it to a key -Let's continue by adding the following generic method that we will use in order -to register our service and any other service that we might work in the future. - -Notice that it removes the Provider key from the name of the service, so for our -service name CalculatorServiceProvider, its key will become -**services.CalculatorService** which matches the +Let's continue by creating a method to register services used by our +application. Notice that we are using `this.serviceProvider` method contributed +by `ServiceMixin`, this method removes the suffix `Provider` from the class name +and uses the remaining string as the binding key. For our service provider +called `CalculatorServiceProvider`, the binding key becomes +**services.CalculatorService** and matches the `@inject('services.CalculatorService')` decorator parameter we used in our controller. @@ -49,18 +49,9 @@ controller. registration for services in the same way we do now for other artifacts in **LB4**. -```ts -service(provider: Constructor>) { - const key = `services.${provider.name.replace(/Provider$/, '')}`; - this.bind(key).toProvider(provider); - } -``` - -Now let's add a method that will make use of this generic `service` method. - ```ts setupServices() { - this.service(CalculatorServiceProvider); + this.serviceProvider(CalculatorServiceProvider); } ``` @@ -72,10 +63,10 @@ constructor after the `this.sequence(MySequence);` statement. this.setupServices(); ``` -**Note:** We could have achieved the above result by just one line inside the -setupServices() method, replacing the generic method. However, the generic one -is more efficient when you need to register multiple services, to keep the -_keys_ standard. +**Note:** We could have achieved the above result by calling the following line +inside the setupServices() method, replacing the method provided by the mixin. +However, the mixin-provided method is more efficient when you need to register +multiple services, to keep the _keys_ standard. ```ts this.bind('services.CalculatorService').toProvider(CalculatorServiceProvider); diff --git a/docs/site/todo-tutorial-geocoding-service.md b/docs/site/todo-tutorial-geocoding-service.md index 81633f68a741..3da83c76488e 100644 --- a/docs/site/todo-tutorial-geocoding-service.md +++ b/docs/site/todo-tutorial-geocoding-service.md @@ -149,8 +149,10 @@ to add few code snippets to our Application class to take care of this task. #### src/application.ts ```ts +import {ServiceMixin} from '@loopback/service-proxy'; + export class TodoListApplication extends BootMixin( - RepositoryMixin(RestApplication), + ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); @@ -162,12 +164,7 @@ export class TodoListApplication extends BootMixin( // ADD THE FOLLOWING TWO METHODS setupServices() { - this.service(GeocoderServiceProvider); - } - - service(provider: Constructor>) { - const key = `services.${provider.name.replace(/Provider$/, '')}`; - this.bind(key).toProvider(provider); + this.serviceProvider(GeocoderServiceProvider); } } ``` diff --git a/examples/soap-calculator/src/application.ts b/examples/soap-calculator/src/application.ts index f5a345f13ad0..f83914987231 100644 --- a/examples/soap-calculator/src/application.ts +++ b/examples/soap-calculator/src/application.ts @@ -1,12 +1,13 @@ import {BootMixin} from '@loopback/boot'; -import {ApplicationConfig, Constructor, Provider} from '@loopback/core'; +import {ApplicationConfig} from '@loopback/core'; import {RepositoryMixin} from '@loopback/repository'; import {RestApplication} from '@loopback/rest'; +import {ServiceMixin} from '@loopback/service-proxy'; import {MySequence} from './sequence'; import {CalculatorServiceProvider} from './services/calculator.service'; export class SoapCalculatorApplication extends BootMixin( - RepositoryMixin(RestApplication), + ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); @@ -30,11 +31,6 @@ export class SoapCalculatorApplication extends BootMixin( } setupServices() { - this.service(CalculatorServiceProvider); - } - - service(provider: Constructor>) { - const key = `services.${provider.name.replace(/Provider$/, '')}`; - this.bind(key).toProvider(provider); + this.serviceProvider(CalculatorServiceProvider); } } diff --git a/examples/todo/src/application.ts b/examples/todo/src/application.ts index 8c5db992351f..8a5b428c5e99 100644 --- a/examples/todo/src/application.ts +++ b/examples/todo/src/application.ts @@ -4,14 +4,15 @@ // License text available at https://opensource.org/licenses/MIT import {BootMixin} from '@loopback/boot'; -import {ApplicationConfig, Constructor, Provider} from '@loopback/core'; +import {ApplicationConfig} from '@loopback/core'; import {RepositoryMixin} from '@loopback/repository'; import {RestApplication} from '@loopback/rest'; +import {ServiceMixin} from '@loopback/service-proxy'; import {MySequence} from './sequence'; import {GeocoderServiceProvider} from './services'; export class TodoListApplication extends BootMixin( - RepositoryMixin(RestApplication), + ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options?: ApplicationConfig) { super(options); @@ -36,14 +37,6 @@ export class TodoListApplication extends BootMixin( } setupServices() { - this.service(GeocoderServiceProvider); - } - - // TODO(bajtos) app.service should be provided either by core Application - // class or a mixin provided by @loopback/service-proxy - // See https://github.com/strongloop/loopback-next/issues/1439 - service(provider: Constructor>) { - const key = `services.${provider.name.replace(/Provider$/, '')}`; - this.bind(key).toProvider(provider); + this.serviceProvider(GeocoderServiceProvider); } } diff --git a/packages/boot/src/booters/datasource.booter.ts b/packages/boot/src/booters/datasource.booter.ts index 556f64ca2c4d..665a626164a1 100644 --- a/packages/boot/src/booters/datasource.booter.ts +++ b/packages/boot/src/booters/datasource.booter.ts @@ -4,7 +4,11 @@ // License text available at https://opensource.org/licenses/MIT import {CoreBindings} from '@loopback/core'; -import {AppWithRepository, juggler, Class} from '@loopback/repository'; +import { + ApplicationWithRepositories, + juggler, + Class, +} from '@loopback/repository'; import {inject} from '@loopback/context'; import {ArtifactOptions} from '../interfaces'; import {BaseArtifactBooter} from './base-artifact.booter'; @@ -22,7 +26,8 @@ import {BootBindings} from '../keys'; */ export class DataSourceBooter extends BaseArtifactBooter { constructor( - @inject(CoreBindings.APPLICATION_INSTANCE) public app: AppWithRepository, + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: ApplicationWithRepositories, @inject(BootBindings.PROJECT_ROOT) public projectRoot: string, @inject(`${BootBindings.BOOT_OPTIONS}#datasources`) public datasourceConfig: ArtifactOptions = {}, diff --git a/packages/boot/src/booters/repository.booter.ts b/packages/boot/src/booters/repository.booter.ts index 1ee22da9f261..a8c2ce841fbf 100644 --- a/packages/boot/src/booters/repository.booter.ts +++ b/packages/boot/src/booters/repository.booter.ts @@ -5,7 +5,7 @@ import {CoreBindings} from '@loopback/core'; import {inject} from '@loopback/context'; -import {AppWithRepository} from '@loopback/repository'; +import {ApplicationWithRepositories} from '@loopback/repository'; import {BaseArtifactBooter} from './base-artifact.booter'; import {BootBindings} from '../keys'; import {ArtifactOptions} from '../interfaces'; @@ -23,7 +23,8 @@ import {ArtifactOptions} from '../interfaces'; */ export class RepositoryBooter extends BaseArtifactBooter { constructor( - @inject(CoreBindings.APPLICATION_INSTANCE) public app: AppWithRepository, + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: ApplicationWithRepositories, @inject(BootBindings.PROJECT_ROOT) public projectRoot: string, @inject(`${BootBindings.BOOT_OPTIONS}#repositories`) public repositoryOptions: ArtifactOptions = {}, diff --git a/packages/boot/test/unit/booters/datasource.booter.unit.ts b/packages/boot/test/unit/booters/datasource.booter.unit.ts index 4f0048dbfde9..c82d99df9468 100644 --- a/packages/boot/test/unit/booters/datasource.booter.unit.ts +++ b/packages/boot/test/unit/booters/datasource.booter.unit.ts @@ -5,7 +5,10 @@ import {expect, TestSandbox, sinon} from '@loopback/testlab'; import {resolve} from 'path'; -import {AppWithRepository, RepositoryMixin} from '@loopback/repository'; +import { + ApplicationWithRepositories, + RepositoryMixin, +} from '@loopback/repository'; import {DataSourceBooter, DataSourceDefaults} from '../../../src'; import {Application} from '@loopback/core'; @@ -33,7 +36,7 @@ describe('datasource booter unit tests', () => { ); const booterInst = new DataSourceBooter( - normalApp as AppWithRepository, + normalApp as ApplicationWithRepositories, SANDBOX_PATH, ); diff --git a/packages/boot/test/unit/booters/repository.booter.unit.ts b/packages/boot/test/unit/booters/repository.booter.unit.ts index 280edef8616d..c63f93c186f5 100644 --- a/packages/boot/test/unit/booters/repository.booter.unit.ts +++ b/packages/boot/test/unit/booters/repository.booter.unit.ts @@ -5,7 +5,10 @@ import {expect, TestSandbox, sinon} from '@loopback/testlab'; import {Application} from '@loopback/core'; -import {RepositoryMixin, AppWithRepository} from '@loopback/repository'; +import { + RepositoryMixin, + ApplicationWithRepositories, +} from '@loopback/repository'; import {RepositoryBooter, RepositoryDefaults} from '../../../index'; import {resolve} from 'path'; @@ -33,7 +36,7 @@ describe('repository booter unit tests', () => { ); const booterInst = new RepositoryBooter( - normalApp as AppWithRepository, + normalApp as ApplicationWithRepositories, SANDBOX_PATH, ); diff --git a/packages/cli/generators/app/index.js b/packages/cli/generators/app/index.js index 0e676740d828..81e0bac3e683 100644 --- a/packages/cli/generators/app/index.js +++ b/packages/cli/generators/app/index.js @@ -12,6 +12,7 @@ module.exports = class AppGenerator extends ProjectGenerator { constructor(args, opts) { super(args, opts); this.buildOptions.push('enableRepository'); + this.buildOptions.push('enableServices'); } _setupGenerator() { @@ -27,6 +28,11 @@ module.exports = class AppGenerator extends ProjectGenerator { description: 'Include repository imports and RepositoryMixin', }); + this.option('enableServices', { + type: Boolean, + description: 'Include service-proxy imports and ServiceMixin', + }); + return super._setupGenerator(); } @@ -80,6 +86,22 @@ module.exports = class AppGenerator extends ProjectGenerator { return super.promptOptions(); } + buildAppClassMixins() { + if (this.shouldExit()) return false; + const {enableRepository, enableServices} = this.projectInfo || {}; + if (!enableRepository && !enableServices) return; + + let appClassWithMixins = 'RestApplication'; + if (enableRepository) { + appClassWithMixins = `RepositoryMixin(${appClassWithMixins})`; + } + if (enableServices) { + appClassWithMixins = `ServiceMixin(${appClassWithMixins})`; + } + + this.projectInfo.appClassWithMixins = appClassWithMixins; + } + scaffold() { return super.scaffold(); } diff --git a/packages/cli/generators/app/templates/src/application.ts.ejs b/packages/cli/generators/app/templates/src/application.ts.ejs index 6fd34c1a2eba..4f48132b77fe 100644 --- a/packages/cli/generators/app/templates/src/application.ts.ejs +++ b/packages/cli/generators/app/templates/src/application.ts.ejs @@ -4,11 +4,19 @@ import {ApplicationConfig} from '@loopback/core'; import {RepositoryMixin} from '@loopback/repository'; <% } -%> import {RestApplication} from '@loopback/rest'; +<% if (project.enableServices) { -%> +import {ServiceMixin} from '@loopback/service-proxy'; +<% } -%> import {MySequence} from './sequence'; -export class <%= project.applicationName %> <% if (!project.enableRepository) {-%>extends BootMixin(RestApplication) {<% } else { -%>extends BootMixin( - RepositoryMixin(RestApplication), +<% if (project.appClassWithMixins) { -%> +export class <%= project.applicationName %> extends BootMixin( + <%= project.appClassWithMixins %>, ) { +<% +} else { // no optional mixins +-%> +export class <%= project.applicationName %> extends BootMixin(RestApplication) { <% } -%> constructor(options?: ApplicationConfig) { super(options); diff --git a/packages/cli/generators/project/templates/package.json.ejs b/packages/cli/generators/project/templates/package.json.ejs index 66a610c81d2f..bd13fcf193ea 100644 --- a/packages/cli/generators/project/templates/package.json.ejs +++ b/packages/cli/generators/project/templates/package.json.ejs @@ -78,9 +78,16 @@ "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>", "@loopback/dist-util": "<%= project.dependencies['@loopback/dist-util'] -%>", "@loopback/openapi-v3": "<%= project.dependencies['@loopback/openapi-v3'] -%>", +<% if (project.enableRepository) { -%> "@loopback/repository": "<%= project.dependencies['@loopback/repository'] -%>", - "@loopback/rest": "<%= project.dependencies['@loopback/rest'] -%>" +<% } -%> +<% if (project.enableServices) { -%> + "@loopback/rest": "<%= project.dependencies['@loopback/rest'] -%>", + "@loopback/service-proxy": "<%= project.dependencies['@loopback/service-proxy'] -%>" <% } else { -%> + "@loopback/rest": "<%= project.dependencies['@loopback/rest'] -%>" +<% } -%> +<% } else { /* NOT AN APPLICATION */-%> "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>", "@loopback/dist-util": "<%= project.dependencies['@loopback/dist-util'] -%>" <% } -%> diff --git a/packages/cli/test/integration/generators/app.integration.js b/packages/cli/test/integration/generators/app.integration.js index 629693aeae29..82442bab4980 100644 --- a/packages/cli/test/integration/generators/app.integration.js +++ b/packages/cli/test/integration/generators/app.integration.js @@ -35,7 +35,7 @@ describe('app-generator specific files', () => { ); assert.fileContent( 'src/application.ts', - /RepositoryMixin\(RestApplication\)/, + /ServiceMixin\(RepositoryMixin\(RestApplication\)\)/, ); assert.fileContent('src/application.ts', /constructor\(/); assert.fileContent('src/application.ts', /this.projectRoot = __dirname/); diff --git a/packages/repository/src/mixins/repository.mixin.ts b/packages/repository/src/mixins/repository.mixin.ts index 70e853bdcb45..8ed4f2957424 100644 --- a/packages/repository/src/mixins/repository.mixin.ts +++ b/packages/repository/src/mixins/repository.mixin.ts @@ -143,7 +143,7 @@ export function RepositoryMixin>(superClass: T) { */ public component(component: Class<{}>) { super.component(component); - this.mountComponentRepository(component); + this.mountComponentRepositories(component); } /** @@ -153,7 +153,7 @@ export function RepositoryMixin>(superClass: T) { * * @param component The component to mount repositories of */ - mountComponentRepository(component: Class<{}>) { + mountComponentRepositories(component: Class<{}>) { const componentKey = `components.${component.name}`; const compInstance = this.getSync(componentKey); @@ -169,7 +169,7 @@ export function RepositoryMixin>(superClass: T) { /** * Interface for an Application mixed in with RepositoryMixin */ -export interface AppWithRepository extends Application { +export interface ApplicationWithRepositories extends Application { // tslint:disable-next-line:no-any repository(repo: Class): void; // tslint:disable-next-line:no-any @@ -179,7 +179,7 @@ export interface AppWithRepository extends Application { name?: string, ): void; component(component: Class<{}>): void; - mountComponentRepository(component: Class<{}>): void; + mountComponentRepositories(component: Class<{}>): void; } /** diff --git a/packages/repository/test/acceptance/has-many.relation.acceptance.ts b/packages/repository/test/acceptance/has-many.relation.acceptance.ts index 5da5cd482767..f834914830d2 100644 --- a/packages/repository/test/acceptance/has-many.relation.acceptance.ts +++ b/packages/repository/test/acceptance/has-many.relation.acceptance.ts @@ -12,7 +12,7 @@ import { hasMany, repository, RepositoryMixin, - AppWithRepository, + ApplicationWithRepositories, HasManyRepositoryFactory, } from '../..'; import {expect} from '@loopback/testlab'; @@ -23,7 +23,7 @@ import {Application} from '@loopback/core'; describe('HasMany relation', () => { // Given a Customer and Order models - see definitions at the bottom - let app: AppWithRepository; + let app: ApplicationWithRepositories; let controller: CustomerController; let customerRepo: CustomerRepository; let orderRepo: OrderRepository; diff --git a/packages/service-proxy/docs.json b/packages/service-proxy/docs.json index 699495ede9e4..05af59883d4e 100644 --- a/packages/service-proxy/docs.json +++ b/packages/service-proxy/docs.json @@ -3,6 +3,7 @@ "./index.ts", "./src/index.ts", "./src/decorators/service.decorator.ts", + "./src/mixins/service.mixin.ts", "./src/legacy-juggler-bridge.ts" ], "codeSectionDepth": 4 diff --git a/packages/service-proxy/src/index.ts b/packages/service-proxy/src/index.ts index 7a9bd70f7eee..2b4349da11a3 100644 --- a/packages/service-proxy/src/index.ts +++ b/packages/service-proxy/src/index.ts @@ -5,3 +5,4 @@ export * from './legacy-juggler-bridge'; export * from './decorators/service.decorator'; +export * from './mixins'; diff --git a/packages/service-proxy/src/mixins/index.ts b/packages/service-proxy/src/mixins/index.ts new file mode 100644 index 000000000000..1a4ff954e2f9 --- /dev/null +++ b/packages/service-proxy/src/mixins/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service-proxy +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './service.mixin'; diff --git a/packages/service-proxy/src/mixins/service.mixin.ts b/packages/service-proxy/src/mixins/service.mixin.ts new file mode 100644 index 000000000000..3fcb754a1a47 --- /dev/null +++ b/packages/service-proxy/src/mixins/service.mixin.ts @@ -0,0 +1,198 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service-proxy +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Provider} from '@loopback/context'; +import {Application} from '@loopback/core'; + +/** + * Interface for classes with `new` operator. + */ +export interface Class { + // new MyClass(...args) ==> T + // tslint:disable-next-line:no-any + new (...args: any[]): T; +} + +/** + * A mixin class for Application that creates a .serviceProvider() + * function to register a service automatically. Also overrides + * component function to allow it to register repositories automatically. + * + * ```ts + * class MyApplication extends ServiceMixin(Application) {} + * ``` + * + * Please note: the members in the mixin function are documented in a dummy class + * called ServiceMixinDoc + * + */ +// tslint:disable-next-line:no-any +export function ServiceMixin>(superClass: T) { + return class extends superClass { + // A mixin class has to take in a type any[] argument! + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + super(...args); + } + + /** + * Add a service to this application. + * + * @param provider The service provider to register. + * + * ```ts + * export interface GeocoderService { + * geocode(address: string): Promise; + * } + * + * export class GeocoderServiceProvider implements Provider { + * constructor( + * @inject('datasources.geocoder') + * protected datasource: juggler.DataSource = new GeocoderDataSource(), + * ) {} + * + * value(): Promise { + * return getService(this.datasource); + * } + * } + * + * app.serviceProvider(GeocoderServiceProvider); + * ``` + */ + serviceProvider(provider: Class>): void { + const serviceName = provider.name.replace(/Provider$/, ''); + const repoKey = `services.${serviceName}`; + this.bind(repoKey) + .toProvider(provider) + .tag('service'); + } + + /** + * Add a component to this application. Also mounts + * all the components services. + * + * @param component The component to add. + * + * ```ts + * + * export class ProductComponent { + * controllers = [ProductController]; + * repositories = [ProductRepo, UserRepo]; + * providers = { + * [AUTHENTICATION_STRATEGY]: AuthStrategy, + * [AUTHORIZATION_ROLE]: Role, + * }; + * }; + * + * app.component(ProductComponent); + * ``` + */ + public component(component: Class<{}>) { + super.component(component); + this.mountComponentServices(component); + } + + /** + * Get an instance of a component and mount all it's + * services. This function is intended to be used internally + * by component() + * + * @param component The component to mount services of + */ + mountComponentServices(component: Class<{}>) { + const componentKey = `components.${component.name}`; + const compInstance = this.getSync(componentKey); + + if (compInstance.serviceProviders) { + for (const provider of compInstance.serviceProviders) { + this.serviceProvider(provider); + } + } + } + }; +} + +/** + * Interface for an Application mixed in with ServiceMixin + */ +export interface ApplicationWithServices extends Application { + // tslint:disable-next-line:no-any + serviceProvider(provider: Class>): void; + component(component: Class<{}>): void; + mountComponentServices(component: Class<{}>): void; +} + +/** + * A dummy class created to generate the tsdoc for the members in service + * mixin. Please don't use it. + * + * The members are implemented in function + * ServiceMixin + */ +export class ServiceMixinDoc { + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + throw new Error( + 'This is a dummy class created for apidoc! Please do not use it!', + ); + } + + /** + * Add a service to this application. + * + * @param provider The service provider to register. + * + * ```ts + * export interface GeocoderService { + * geocode(address: string): Promise; + * } + * + * export class GeocoderServiceProvider implements Provider { + * constructor( + * @inject('datasources.geocoder') + * protected datasource: juggler.DataSource = new GeocoderDataSource(), + * ) {} + * + * value(): Promise { + * return getService(this.datasource); + * } + * } + * + * app.serviceProvider(GeocoderServiceProvider); + * ``` + */ + serviceProvider(provider: Class>): void {} + + /** + * Add a component to this application. Also mounts + * all the components services. + * + * @param component The component to add. + * + * ```ts + * + * export class ProductComponent { + * controllers = [ProductController]; + * repositories = [ProductRepo, UserRepo]; + * providers = { + * [AUTHENTICATION_STRATEGY]: AuthStrategy, + * [AUTHORIZATION_ROLE]: Role, + * }; + * }; + * + * app.component(ProductComponent); + * ``` + */ + public component(component: Class<{}>) {} + + /** + * Get an instance of a component and mount all it's + * services. This function is intended to be used internally + * by component() + * + * @param component The component to mount services of + */ + mountComponentServices(component: Class<{}>) {} +} diff --git a/packages/service-proxy/test/unit/mixin/service.mixin.unit.ts b/packages/service-proxy/test/unit/mixin/service.mixin.unit.ts new file mode 100644 index 000000000000..c581f53548b3 --- /dev/null +++ b/packages/service-proxy/test/unit/mixin/service.mixin.unit.ts @@ -0,0 +1,97 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service-proxy +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application, Component, Provider} from '@loopback/core'; +import {expect} from '@loopback/testlab'; +import {Class, ServiceMixin} from '../../../'; + +// tslint:disable:no-any + +describe('ServiceMixin', () => { + it('mixed class has .serviceProvider()', () => { + const myApp = new AppWithServiceMixin(); + expect(typeof myApp.serviceProvider).to.be.eql('function'); + }); + + it('binds repository from app.serviceProvider()', async () => { + const myApp = new AppWithServiceMixin(); + + expectGeocoderToNotBeBound(myApp); + myApp.serviceProvider(GeocoderServiceProvider); + await expectGeocoderToBeBound(myApp); + }); + + it('binds a component without services', () => { + class EmptyTestComponent {} + + const myApp = new AppWithServiceMixin(); + myApp.component(EmptyTestComponent); + + expectComponentToBeBound(myApp, EmptyTestComponent); + }); + + it('binds a component with a service provider from .component()', async () => { + const myApp = new AppWithServiceMixin(); + + const boundComponentsBefore = myApp.find('components.*').map(b => b.key); + expect(boundComponentsBefore).to.be.empty(); + expectGeocoderToNotBeBound(myApp); + + myApp.component(GeocoderComponent); + + expectComponentToBeBound(myApp, GeocoderComponent); + await expectGeocoderToBeBound(myApp); + }); + + class AppWithServiceMixin extends ServiceMixin(Application) {} + + interface GeoPoint { + lat: number; + lng: number; + } + + interface GeocoderService { + geocode(address: string): Promise; + } + + // A dummy service instance to make unit testing easier + const GeocoderSingleton: GeocoderService = { + geocode(address: string) { + return Promise.resolve({lat: 0, lng: 0}); + }, + }; + + class GeocoderServiceProvider implements Provider { + value(): Promise { + return Promise.resolve(GeocoderSingleton); + } + } + + class GeocoderComponent { + serviceProviders = [GeocoderServiceProvider]; + } + + async function expectGeocoderToBeBound(myApp: Application) { + const boundRepositories = myApp.find('services.*').map(b => b.key); + expect(boundRepositories).to.containEql('services.GeocoderService'); + const repoInstance = await myApp.get('services.GeocoderService'); + expect(repoInstance).to.equal(GeocoderSingleton); + } + + function expectGeocoderToNotBeBound(myApp: Application) { + const boundRepos = myApp.find('services.*').map(b => b.key); + expect(boundRepos).to.be.empty(); + } + + function expectComponentToBeBound( + myApp: Application, + component: Class, + ) { + const boundComponents = myApp.find('components.*').map(b => b.key); + expect(boundComponents).to.containEql(`components.${component.name}`); + const componentInstance = myApp.getSync(`components.${component.name}`); + expect(componentInstance).to.be.instanceOf(component); + } +});