Skip to content

Commit

Permalink
feat(service-proxy): add service mixin
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bajtos committed Aug 28, 2018
1 parent be71ece commit fb01931
Show file tree
Hide file tree
Showing 20 changed files with 407 additions and 89 deletions.
21 changes: 5 additions & 16 deletions docs/site/Calling-other-APIs-and-Web-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -162,18 +149,20 @@ export class GeoServiceProvider implements Provider<GeoService> {
}
```

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
constructor:

```ts
export class MyController {
@inject('services.geo')
@inject('services.GeoService')
private geoService: GeoService;
}
```
Expand Down
49 changes: 20 additions & 29 deletions docs/site/soap-calculator-tutorial-register-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,43 @@ 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.

**NOTE:** This will be the method for now until we place the autodiscover and
registration for services in the same way we do now for other artifacts in
**LB4**.

```ts
service<T>(provider: Constructor<Provider<T>>) {
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<T>` method.

```ts
setupServices() {
this.service(CalculatorServiceProvider);
this.serviceProvider(CalculatorServiceProvider);
}
```

Expand All @@ -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);
Expand Down
11 changes: 4 additions & 7 deletions docs/site/todo-tutorial-geocoding-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -162,12 +164,7 @@ export class TodoListApplication extends BootMixin(

// ADD THE FOLLOWING TWO METHODS
setupServices() {
this.service(GeocoderServiceProvider);
}

service<T>(provider: Constructor<Provider<T>>) {
const key = `services.${provider.name.replace(/Provider$/, '')}`;
this.bind(key).toProvider(provider);
this.serviceProvider(GeocoderServiceProvider);
}
}
```
Expand Down
12 changes: 4 additions & 8 deletions examples/soap-calculator/src/application.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -30,11 +31,6 @@ export class SoapCalculatorApplication extends BootMixin(
}

setupServices() {
this.service(CalculatorServiceProvider);
}

service<T>(provider: Constructor<Provider<T>>) {
const key = `services.${provider.name.replace(/Provider$/, '')}`;
this.bind(key).toProvider(provider);
this.serviceProvider(CalculatorServiceProvider);
}
}
15 changes: 4 additions & 11 deletions examples/todo/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<T>(provider: Constructor<Provider<T>>) {
const key = `services.${provider.name.replace(/Provider$/, '')}`;
this.bind(key).toProvider(provider);
this.serviceProvider(GeocoderServiceProvider);
}
}
9 changes: 7 additions & 2 deletions packages/boot/src/booters/datasource.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = {},
Expand Down
5 changes: 3 additions & 2 deletions packages/boot/src/booters/repository.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = {},
Expand Down
7 changes: 5 additions & 2 deletions packages/boot/test/unit/booters/datasource.booter.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -33,7 +36,7 @@ describe('datasource booter unit tests', () => {
);

const booterInst = new DataSourceBooter(
normalApp as AppWithRepository,
normalApp as ApplicationWithRepositories,
SANDBOX_PATH,
);

Expand Down
7 changes: 5 additions & 2 deletions packages/boot/test/unit/booters/repository.booter.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -33,7 +36,7 @@ describe('repository booter unit tests', () => {
);

const booterInst = new RepositoryBooter(
normalApp as AppWithRepository,
normalApp as ApplicationWithRepositories,
SANDBOX_PATH,
);

Expand Down
22 changes: 22 additions & 0 deletions packages/cli/generators/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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();
}

Expand Down Expand Up @@ -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();
}
Expand Down
12 changes: 10 additions & 2 deletions packages/cli/generators/app/templates/src/application.ts.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/generators/project/templates/package.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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'] -%>"
<% } -%>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand Down
Loading

0 comments on commit fb01931

Please sign in to comment.