Skip to content

Commit 48e01f4

Browse files
committed
feat(boot): improve service booter to load classes decorated with @Bind
1 parent 88eff77 commit 48e01f4

File tree

5 files changed

+102
-34
lines changed

5 files changed

+102
-34
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright IBM Corp. 2019. All Rights Reserved.
2+
// Node module: @loopback/boot
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {bind, BindingScope, Provider} from '@loopback/core';
7+
8+
@bind({
9+
tags: {serviceType: 'local'},
10+
scope: BindingScope.SINGLETON,
11+
})
12+
export class BindableGreetingService {
13+
greet(whom: string = 'world') {
14+
return Promise.resolve(`Hello ${whom}`);
15+
}
16+
}
17+
18+
@bind({tags: {serviceType: 'local', name: 'CurrentDate'}})
19+
export class DateProvider implements Provider<Date> {
20+
value(): Promise<Date> {
21+
return Promise.resolve(new Date());
22+
}
23+
}
24+
25+
export class NotBindableGreetingService {
26+
greet(whom: string = 'world') {
27+
return Promise.resolve(`Hello ${whom}`);
28+
}
29+
}
30+
31+
export class NotBindableDateProvider implements Provider<Date> {
32+
value(): Promise<Date> {
33+
return Promise.resolve(new Date());
34+
}
35+
}

packages/boot/src/__tests__/integration/service.booter.integration.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ describe('service booter integration tests', () => {
2121

2222
it('boots services when app.boot() is called', async () => {
2323
const expectedBindings = [
24-
`${SERVICES_PREFIX}.GeocoderService`,
25-
// greeting service is skipped - service classes are not supported (yet)
24+
'services.BindableGreetingService',
25+
'services.CurrentDate',
26+
'services.GeocoderService',
27+
'services.NotBindableDate',
2628
];
2729

2830
await app.boot();
@@ -31,6 +33,18 @@ describe('service booter integration tests', () => {
3133
expect(bindings.sort()).to.eql(expectedBindings.sort());
3234
});
3335

36+
it('boots bindable classes when app.boot() is called', async () => {
37+
const expectedBindings = [
38+
`${SERVICES_PREFIX}.CurrentDate`,
39+
`${SERVICES_PREFIX}.BindableGreetingService`,
40+
];
41+
42+
await app.boot();
43+
44+
const bindings = app.findByTag({serviceType: 'local'}).map(b => b.key);
45+
expect(bindings.sort()).to.eql(expectedBindings.sort());
46+
});
47+
3448
async function getApp() {
3549
await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js'));
3650
await sandbox.copyFile(
@@ -43,6 +57,11 @@ describe('service booter integration tests', () => {
4357
'services/greeting.service.js',
4458
);
4559

60+
await sandbox.copyFile(
61+
resolve(__dirname, '../fixtures/bindable-classes.artifact.js'),
62+
'services/bindable-classes.service.js',
63+
);
64+
4665
const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp;
4766
app = new MyApp();
4867
}

packages/boot/src/__tests__/unit/booters/service.booter.unit.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('service booter unit tests', () => {
2727
beforeEach(createStub);
2828
afterEach(restoreStub);
2929

30-
it('gives a warning if called on an app without RepositoryMixin', async () => {
30+
it('does not require service mixin', async () => {
3131
const normalApp = new Application();
3232
await sandbox.copyFile(
3333
resolve(__dirname, '../../fixtures/service-provider.artifact.js'),
@@ -43,12 +43,7 @@ describe('service booter unit tests', () => {
4343
];
4444
await booterInst.load();
4545

46-
sinon.assert.calledOnce(stub);
47-
sinon.assert.calledWith(
48-
stub,
49-
'app.serviceProvider() function is needed for ServiceBooter. You can add ' +
50-
'it to your Application using ServiceMixin from @loopback/service-proxy.',
51-
);
46+
sinon.assert.notCalled(stub);
5247
});
5348

5449
it(`uses ServiceDefaults for 'options' if none are given`, () => {

packages/boot/src/booters/service.booter.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6+
import {
7+
BINDING_METADATA_KEY,
8+
Constructor,
9+
inject,
10+
MetadataInspector,
11+
} from '@loopback/context';
612
import {CoreBindings} from '@loopback/core';
713
import {ApplicationWithServices} from '@loopback/service-proxy';
8-
import {inject, Provider, Constructor} from '@loopback/context';
14+
import * as debugFactory from 'debug';
915
import {ArtifactOptions} from '../interfaces';
10-
import {BaseArtifactBooter} from './base-artifact.booter';
1116
import {BootBindings} from '../keys';
17+
import {BaseArtifactBooter} from './base-artifact.booter';
1218

13-
type ServiceProviderClass = Constructor<Provider<object>>;
19+
const debug = debugFactory('loopback:boot:service-booter');
1420

1521
/**
1622
* A class that extends BaseArtifactBooter to boot the 'Service' artifact type.
@@ -23,8 +29,6 @@ type ServiceProviderClass = Constructor<Provider<object>>;
2329
* @param bootConfig - Service Artifact Options Object
2430
*/
2531
export class ServiceBooter extends BaseArtifactBooter {
26-
serviceProviders: ServiceProviderClass[];
27-
2832
constructor(
2933
@inject(CoreBindings.APPLICATION_INSTANCE)
3034
public app: ApplicationWithServices,
@@ -46,24 +50,12 @@ export class ServiceBooter extends BaseArtifactBooter {
4650
async load() {
4751
await super.load();
4852

49-
this.serviceProviders = this.classes.filter(isServiceProvider);
53+
for (const cls of this.classes) {
54+
if (!isBindableClass(cls)) continue;
5055

51-
/**
52-
* If Service providers were discovered, we need to make sure ServiceMixin
53-
* was used (so we have `app.serviceProvider()`) to perform the binding of a
54-
* Service provider class.
55-
*/
56-
if (this.serviceProviders.length > 0) {
57-
if (!this.app.serviceProvider) {
58-
console.warn(
59-
'app.serviceProvider() function is needed for ServiceBooter. You can add ' +
60-
'it to your Application using ServiceMixin from @loopback/service-proxy.',
61-
);
62-
} else {
63-
this.serviceProviders.forEach(cls => {
64-
this.app.serviceProvider(cls as Constructor<Provider<object>>);
65-
});
66-
}
56+
debug('Bind class: %s', cls.name);
57+
const binding = this.app.service(cls);
58+
debug('Binding created for class: %j', binding);
6759
}
6860
}
6961
}
@@ -77,8 +69,20 @@ export const ServiceDefaults: ArtifactOptions = {
7769
nested: true,
7870
};
7971

80-
function isServiceProvider(cls: Constructor<{}>): cls is ServiceProviderClass {
72+
function isServiceProvider(cls: Constructor<unknown>) {
8173
const hasSupportedName = cls.name.endsWith('Provider');
82-
const hasValueMethod = 'value' in cls.prototype;
74+
const hasValueMethod = typeof cls.prototype.value === 'function';
8375
return hasSupportedName && hasValueMethod;
8476
}
77+
78+
function isBindableClass(cls: Constructor<unknown>) {
79+
if (MetadataInspector.getClassMetadata(BINDING_METADATA_KEY, cls)) {
80+
return true;
81+
}
82+
if (isServiceProvider(cls)) {
83+
debug('Provider class found: %s', cls.name);
84+
return true;
85+
}
86+
debug('Skip class not decorated with @bind: %s', cls.name);
87+
return false;
88+
}

packages/core/src/__tests__/unit/application.unit.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,26 @@ describe('Application', () => {
270270
const binding = app.service(MyServiceProvider);
271271
expect(Array.from(binding.tagNames)).to.containEql(CoreTags.SERVICE);
272272
expect(binding.tagMap.date).to.eql('now');
273-
expect(binding.key).to.equal('localServices.MyServiceProvider');
273+
expect(binding.key).to.equal('localServices.MyService');
274274
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
275275
expect(findKeysByTag(app, 'service')).to.containEql(binding.key);
276276
});
277277

278+
it('binds a service provider with name tag', () => {
279+
@bind({tags: {date: 'now', name: 'my-service'}})
280+
class MyServiceProvider implements Provider<Date> {
281+
value() {
282+
return new Date();
283+
}
284+
}
285+
286+
const binding = app.service(MyServiceProvider);
287+
expect(Array.from(binding.tagNames)).to.containEql(CoreTags.SERVICE);
288+
expect(binding.tagMap.date).to.eql('now');
289+
expect(binding.key).to.equal('services.my-service');
290+
expect(findKeysByTag(app, 'service')).to.containEql(binding.key);
291+
});
292+
278293
function givenApp() {
279294
app = new Application();
280295
}

0 commit comments

Comments
 (0)