Skip to content

Commit 88eff77

Browse files
committed
feat(core): add app.service() to register service classes or providers
1 parent 0041d38 commit 88eff77

File tree

4 files changed

+131
-9
lines changed

4 files changed

+131
-9
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,57 @@ describe('Application', () => {
229229
}
230230
});
231231

232+
describe('service binding', () => {
233+
let app: Application;
234+
class MyService {}
235+
236+
beforeEach(givenApp);
237+
238+
it('binds a service', () => {
239+
const binding = app.service(MyService);
240+
expect(Array.from(binding.tagNames)).to.containEql(CoreTags.SERVICE);
241+
expect(binding.key).to.equal('services.MyService');
242+
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
243+
expect(findKeysByTag(app, CoreTags.SERVICE)).to.containEql(binding.key);
244+
});
245+
246+
it('binds a service with custom name', () => {
247+
const binding = app.service(MyService, 'my-service');
248+
expect(Array.from(binding.tagNames)).to.containEql(CoreTags.SERVICE);
249+
expect(binding.key).to.equal('services.my-service');
250+
expect(findKeysByTag(app, CoreTags.SERVICE)).to.containEql(binding.key);
251+
});
252+
253+
it('binds a singleton service', () => {
254+
@bind({scope: BindingScope.SINGLETON})
255+
class MySingletonService {}
256+
257+
const binding = app.service(MySingletonService);
258+
expect(binding.scope).to.equal(BindingScope.SINGLETON);
259+
expect(findKeysByTag(app, 'service')).to.containEql(binding.key);
260+
});
261+
262+
it('binds a service provider', () => {
263+
@bind({tags: {date: 'now', namespace: 'localServices'}})
264+
class MyServiceProvider implements Provider<Date> {
265+
value() {
266+
return new Date();
267+
}
268+
}
269+
270+
const binding = app.service(MyServiceProvider);
271+
expect(Array.from(binding.tagNames)).to.containEql(CoreTags.SERVICE);
272+
expect(binding.tagMap.date).to.eql('now');
273+
expect(binding.key).to.equal('localServices.MyServiceProvider');
274+
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
275+
expect(findKeysByTag(app, 'service')).to.containEql(binding.key);
276+
});
277+
278+
function givenApp() {
279+
app = new Application();
280+
}
281+
});
282+
232283
function findKeysByTag(ctx: Context, tag: string | RegExp) {
233284
return ctx.findByTag(tag).map(binding => binding.key);
234285
}

packages/core/src/application.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
import {
77
Binding,
88
BindingScope,
9+
bindingTemplateFor,
910
Constructor,
1011
Context,
12+
ContextTags,
1113
createBindingFromClass,
14+
isProviderClass,
15+
Provider,
1216
} from '@loopback/context';
1317
import * as debugFactory from 'debug';
1418
import {Component, mountComponent} from './component';
@@ -241,6 +245,73 @@ export class Application extends Context implements LifeCycleObserver {
241245
this.add(binding);
242246
return binding;
243247
}
248+
249+
/**
250+
* Add a service to this application.
251+
*
252+
* @param cls - The service or provider class
253+
*
254+
* @example
255+
*
256+
* ```ts
257+
* // Define a class to be bound via ctx.toClass()
258+
* @bind({scope: BindingScope.SINGLETON})
259+
* export class LogService {
260+
* log(msg: string) {
261+
* console.log(msg);
262+
* }
263+
* }
264+
*
265+
* // Define a class to be bound via ctx.toProvider()
266+
* const uuidv4 = require('uuid/v4');
267+
* export class UuidProvider implements Provider<string> {
268+
* value() {
269+
* return uuidv4();
270+
* }
271+
* }
272+
*
273+
* // Register the local services
274+
* app.service(LogService);
275+
* app.service(UuidProvider, 'uuid');
276+
*
277+
* export class MyController {
278+
* constructor(
279+
* @inject('services.uuid') private uuid: string,
280+
* @inject('services.LogService') private log: LogService,
281+
* ) {
282+
* }
283+
*
284+
* greet(name: string) {
285+
* this.log(`Greet request ${this.uuid} received: ${name}`);
286+
* return `${this.uuid}: ${name}`;
287+
* }
288+
* }
289+
* ```
290+
*/
291+
public service<S>(
292+
cls: Constructor<S> | Constructor<Provider<S>>,
293+
name?: string,
294+
): Binding<S> {
295+
if (!name && isProviderClass(cls)) {
296+
// Trim `Provider` from the default service name
297+
// This is needed to keep backward compatibility
298+
const templateFn = bindingTemplateFor(cls);
299+
const template = Binding.bind<S>('template').apply(templateFn);
300+
if (
301+
template.tagMap[ContextTags.PROVIDER] &&
302+
!template.tagMap[ContextTags.NAME]
303+
) {
304+
// The class is a provider and no `name` tag is found
305+
name = cls.name.replace(/Provider$/, '');
306+
}
307+
}
308+
const binding = createBindingFromClass(cls, {
309+
name,
310+
type: 'service',
311+
});
312+
this.add(binding);
313+
return binding;
314+
}
244315
}
245316

246317
/**

packages/core/src/keys.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ export namespace CoreTags {
111111
*/
112112
export const CONTROLLER = 'controller';
113113

114+
/**
115+
* Binding tag for services
116+
*/
117+
export const SERVICE = 'service';
118+
114119
/**
115120
* Binding tag for life cycle observers
116121
*/

packages/service-proxy/src/mixins/service.mixin.ts

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

6-
import {Binding, createBindingFromClass, Provider} from '@loopback/context';
6+
import {Binding, Provider} from '@loopback/context';
77
import {Application} from '@loopback/core';
88

99
/**
@@ -41,6 +41,8 @@ export function ServiceMixin<T extends Class<any>>(superClass: T) {
4141
/**
4242
* Add a service to this application.
4343
*
44+
* @deprecated Use app.service() instead
45+
*
4446
* @param provider - The service provider to register.
4547
*
4648
* @example
@@ -67,14 +69,7 @@ export function ServiceMixin<T extends Class<any>>(superClass: T) {
6769
provider: Class<Provider<S>>,
6870
name?: string,
6971
): Binding<S> {
70-
const serviceName = name || provider.name.replace(/Provider$/, '');
71-
const binding = createBindingFromClass(provider, {
72-
name: serviceName,
73-
namespace: 'services',
74-
type: 'service',
75-
});
76-
this.add(binding);
77-
return binding;
72+
return this.service(provider, name);
7873
}
7974

8075
/**

0 commit comments

Comments
 (0)