Skip to content

Commit

Permalink
feat(boot): implement Service booter
Browse files Browse the repository at this point in the history
  • Loading branch information
bajtos committed Aug 30, 2018
1 parent be0c618 commit 36a8eac
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 5 deletions.
24 changes: 24 additions & 0 deletions packages/boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ List of Options available on BootOptions Object.
| `controllers` | `ArtifactOptions` | ControllerBooter convention options |
| `repositories` | `ArtifactOptions` | RepositoryBooter convention options |
| `datasources` | `ArtifactOptions` | DataSourceBooter convention options |
| `services` | `ArtifactOptions` | ServiceBooter convention options |

### ArtifactOptions

Expand Down Expand Up @@ -159,6 +160,29 @@ Available options on the `datasources` object on `BootOptions` are as follows:
| `nested` | `boolean` | `true` | Look in nested directories in `dirs` for DataSource artifacts |
| `glob` | `string` | | A `glob` pattern string. This takes precedence over above 3 options (which are used to make a glob pattern). |

### ServiceBooter

#### Description

Discovers and binds Service providers using `app.serviceProvider()` (Application
must use `ServiceMixin` from `@loopback/service-proxy`).

#### Options

The options for this can be passed via `BootOptions` when calling
`app.boot(options: BootOptions)`.

The options for this are passed in a `services` object on `BootOptions`.

Available options on the `services` object on `BootOptions` are as follows:

| Options | Type | Default | Description |
| ------------ | -------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------ |
| `dirs` | `string \| string[]` | `['repositories']` | Paths relative to projectRoot to look in for Service artifacts |
| `extensions` | `string \| string[]` | `['.repository.js']` | File extensions to match for Service artifacts |
| `nested` | `boolean` | `true` | Look in nested directories in `dirs` for Service artifacts |
| `glob` | `string` | | A `glob` pattern string. This takes precedence over above 3 options (which are used to make a glob pattern). |

## Contributions

- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md)
Expand Down
1 change: 1 addition & 0 deletions packages/boot/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"src/booters/controller.booter.ts",
"src/booters/datasource.booter.ts",
"src/booters/repository.booter.ts",
"src/booters/service.booter.ts",
"src/booters/index.ts",
"src/mixins/boot.mixin.ts",
"src/mixins/index.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/boot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@loopback/core": "^0.11.5",
"@loopback/dist-util": "^0.3.6",
"@loopback/repository": "^0.15.1",
"@loopback/service-proxy": "^0.7.1",
"@types/debug": "0.0.30",
"@types/glob": "^5.0.35",
"debug": "^3.1.0",
Expand Down
14 changes: 12 additions & 2 deletions packages/boot/src/boot.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import {Bootstrapper} from './bootstrapper';
import {Component, Application, CoreBindings} from '@loopback/core';
import {inject, BindingScope} from '@loopback/context';
import {ControllerBooter, RepositoryBooter, DataSourceBooter} from './booters';
import {
ControllerBooter,
RepositoryBooter,
DataSourceBooter,
ServiceBooter,
} from './booters';
import {BootBindings} from './keys';

/**
Expand All @@ -17,7 +22,12 @@ import {BootBindings} from './keys';
export class BootComponent implements Component {
// Export a list of default booters in the component so they get bound
// automatically when this component is mounted.
booters = [ControllerBooter, RepositoryBooter, DataSourceBooter];
booters = [
ControllerBooter,
RepositoryBooter,
ServiceBooter,
DataSourceBooter,
];

/**
*
Expand Down
3 changes: 2 additions & 1 deletion packages/boot/src/booters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
export * from './base-artifact.booter';
export * from './booter-utils';
export * from './controller.booter';
export * from './repository.booter';
export * from './datasource.booter';
export * from './repository.booter';
export * from './service.booter';
82 changes: 82 additions & 0 deletions packages/boot/src/booters/service.booter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {CoreBindings} from '@loopback/core';
import {ApplicationWithServices} from '@loopback/service-proxy';
import {inject, Provider, Constructor} from '@loopback/context';
import {ArtifactOptions} from '../interfaces';
import {BaseArtifactBooter} from './base-artifact.booter';
import {BootBindings} from '../keys';

type ServiceProviderClass = Constructor<Provider<object>>;

/**
* A class that extends BaseArtifactBooter to boot the 'DataSource' artifact type.
* Discovered DataSources are bound using `app.controller()`.
*
* Supported phases: configure, discover, load
*
* @param app Application instance
* @param projectRoot Root of User Project relative to which all paths are resolved
* @param [bootConfig] DataSource Artifact Options Object
*/
export class ServiceBooter extends BaseArtifactBooter {
serviceProviders: ServiceProviderClass[];

constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
public app: ApplicationWithServices,
@inject(BootBindings.PROJECT_ROOT) projectRoot: string,
@inject(`${BootBindings.BOOT_OPTIONS}#services`)
public serviceConfig: ArtifactOptions = {},
) {
super(
projectRoot,
// Set DataSource Booter Options if passed in via bootConfig
Object.assign({}, ServiceDefaults, serviceConfig),
);
}

/**
* Uses super method to get a list of Artifact classes. Boot each file by
* creating a DataSourceConstructor and binding it to the application class.
*/
async load() {
await super.load();

this.serviceProviders = this.classes.filter(isServiceProvider);

/**
* If Service providers were discovered, we need to make sure ServiceMixin
* was used (so we have `app.serviceProvider()`) to perform the binding of a
* Service provider class.
*/
if (this.serviceProviders.length > 0) {
if (!this.app.serviceProvider) {
console.warn(
'app.serviceProvider() function is needed for ServiceBooter. You can add ' +
'it to your Application using ServiceMixin from @loopback/service-proxy.',
);
} else {
this.serviceProviders.forEach(cls => {
this.app.serviceProvider(cls as Constructor<Provider<object>>);
});
}
}
}
}

/**
* Default ArtifactOptions for DataSourceBooter.
*/
export const ServiceDefaults: ArtifactOptions = {
dirs: ['services'],
extensions: ['.service.js'],
nested: true,
};

function isServiceProvider(cls: Constructor<{}>): cls is ServiceProviderClass {
return /Provider$/.test(cls.name);
}
5 changes: 4 additions & 1 deletion packages/boot/test/fixtures/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
import {ApplicationConfig} from '@loopback/core';
import {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {ServiceMixin} from '@loopback/service-proxy';
import {BootMixin} from '../../index';

export class BooterApp extends RepositoryMixin(BootMixin(RestApplication)) {
export class BooterApp extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options?: ApplicationConfig) {
super(options);
this.projectRoot = __dirname;
Expand Down
10 changes: 10 additions & 0 deletions packages/boot/test/fixtures/service-class.artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export class GreetingService {
greet(whom: string = 'world') {
return Promise.resolve(`Hello ${whom}`);
}
}
28 changes: 28 additions & 0 deletions packages/boot/test/fixtures/service-provider.artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Provider} from '@loopback/core';

export interface GeoPoint {
lat: number;
lng: number;
}

export interface GeocoderService {
geocode(address: string): Promise<GeoPoint>;
}

// A dummy service instance to make unit testing easier
const GeocoderSingleton: GeocoderService = {
geocode(address: string) {
return Promise.resolve({lat: 0, lng: 0});
},
};

export class GeocoderServiceProvider implements Provider<GeocoderService> {
value(): Promise<GeocoderService> {
return Promise.resolve(GeocoderSingleton);
}
}
49 changes: 49 additions & 0 deletions packages/boot/test/integration/service.booter.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect, TestSandbox} from '@loopback/testlab';
import {resolve} from 'path';
import {BooterApp} from '../fixtures/application';

describe('service booter integration tests', () => {
const SANDBOX_PATH = resolve(__dirname, '../../.sandbox');
const sandbox = new TestSandbox(SANDBOX_PATH);

const SERVICES_PREFIX = 'services';
const SERVICES_TAG = 'service';

let app: BooterApp;

beforeEach('reset sandbox', () => sandbox.reset());
beforeEach(getApp);

it('boots services when app.boot() is called', async () => {
const expectedBindings = [
`${SERVICES_PREFIX}.GeocoderService`,
// greeting service is skipped - service classes are not supported (yet)
];

await app.boot();

const bindings = app.findByTag(SERVICES_TAG).map(b => b.key);
expect(bindings.sort()).to.eql(expectedBindings.sort());
});

async function getApp() {
await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js'));
await sandbox.copyFile(
resolve(__dirname, '../fixtures/service-provider.artifact.js'),
'services/geocoder.service.js',
);

await sandbox.copyFile(
resolve(__dirname, '../fixtures/service-class.artifact.js'),
'services/greeting.service.js',
);

const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp;
app = new MyApp();
}
});
8 changes: 8 additions & 0 deletions packages/boot/test/unit/boot.component.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
BootMixin,
RepositoryBooter,
DataSourceBooter,
ServiceBooter,
} from '../../';

describe('boot.component unit tests', () => {
Expand Down Expand Up @@ -47,6 +48,13 @@ describe('boot.component unit tests', () => {
expect(booterInst).to.be.an.instanceOf(DataSourceBooter);
});

it('ServiceBooter is bound as a booter by default', async () => {
const booterInst = await app.get(
`${BootBindings.BOOTER_PREFIX}.ServiceBooter`,
);
expect(booterInst).to.be.an.instanceOf(ServiceBooter);
});

function getApp() {
app = new BootableApp();
app.bind(BootBindings.PROJECT_ROOT).to(__dirname);
Expand Down
2 changes: 1 addition & 1 deletion packages/boot/test/unit/booters/datasource.booter.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('datasource booter unit tests', () => {
beforeEach(createStub);
afterEach(restoreStub);

it('gives a wanring if called on an app without RepositoryMixin', async () => {
it('gives a warning if called on an app without RepositoryMixin', async () => {
const normalApp = new Application();
await sandbox.copyFile(
resolve(__dirname, '../../fixtures/datasource.artifact.js'),
Expand Down
Loading

0 comments on commit 36a8eac

Please sign in to comment.