diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts index e5474f0eace0..18ba2840d46b 100644 --- a/packages/boot/src/boot.component.ts +++ b/packages/boot/src/boot.component.ts @@ -3,16 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Bootstrapper} from './bootstrapper'; -import {Component, Application, CoreBindings} from '@loopback/core'; -import {inject, BindingScope} from '@loopback/context'; +import {BindingScope, inject} from '@loopback/context'; +import {Application, Component, CoreBindings} from '@loopback/core'; import { + ApplicationMetadataBooter, ControllerBooter, - RepositoryBooter, DataSourceBooter, + RepositoryBooter, ServiceBooter, - ApplicationMetadataBooter, + LifeCycleObserverBooter, } from './booters'; +import {Bootstrapper} from './bootstrapper'; import {BootBindings} from './keys'; /** @@ -29,6 +30,7 @@ export class BootComponent implements Component { RepositoryBooter, ServiceBooter, DataSourceBooter, + LifeCycleObserverBooter, ]; /** diff --git a/packages/boot/src/booters/index.ts b/packages/boot/src/booters/index.ts index 6bb4f8676110..fb5057c6e7b8 100644 --- a/packages/boot/src/booters/index.ts +++ b/packages/boot/src/booters/index.ts @@ -10,3 +10,4 @@ export * from './datasource.booter'; export * from './repository.booter'; export * from './service.booter'; export * from './application-metadata.booter'; +export * from './lifecyle-observer.booter'; diff --git a/packages/boot/src/booters/lifecyle-observer.booter.ts b/packages/boot/src/booters/lifecyle-observer.booter.ts new file mode 100644 index 000000000000..c903b802fa34 --- /dev/null +++ b/packages/boot/src/booters/lifecyle-observer.booter.ts @@ -0,0 +1,73 @@ +// 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 {Constructor, inject, createBindingFromClass} from '@loopback/context'; +import { + Application, + asLifeCycleObserverBinding, + CoreBindings, + isLifeCycleObserverClass, + LifeCycleObserver, + CoreTags, +} from '@loopback/core'; +import {ArtifactOptions} from '../interfaces'; +import {BootBindings} from '../keys'; +import {BaseArtifactBooter} from './base-artifact.booter'; + +import * as debugFactory from 'debug'; +const debug = debugFactory('loopback:boot:lifecycle-observer-booter'); + +type LifeCycleObserverClass = Constructor; + +/** + * A class that extends BaseArtifactBooter to boot the 'LifeCycleObserver' artifact type. + * + * Supported phases: configure, discover, load + * + * @param app Application instance + * @param projectRoot Root of User Project relative to which all paths are resolved + * @param [bootConfig] LifeCycleObserver Artifact Options Object + */ +export class LifeCycleObserverBooter extends BaseArtifactBooter { + observers: LifeCycleObserverClass[]; + + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: Application, + @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(`${BootBindings.BOOT_OPTIONS}#observers`) + public observerConfig: ArtifactOptions = {}, + ) { + super( + projectRoot, + // Set LifeCycleObserver Booter Options if passed in via bootConfig + Object.assign({}, LifeCycleObserverDefaults, observerConfig), + ); + } + + /** + * 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.observers = this.classes.filter(isLifeCycleObserverClass); + for (const observer of this.observers) { + debug('Bind life cycle observer: %s', observer.name); + const binding = this.app.lifeCycleObserver(observer); + debug('Binding created for life cycle observer: %j', binding); + } + } +} + +/** + * Default ArtifactOptions for DataSourceBooter. + */ +export const LifeCycleObserverDefaults: ArtifactOptions = { + dirs: ['observers'], + extensions: ['.observer.js'], + nested: true, +}; diff --git a/packages/boot/src/booters/service.booter.ts b/packages/boot/src/booters/service.booter.ts index b897babb9e68..a5f4f9ab4ad0 100644 --- a/packages/boot/src/booters/service.booter.ts +++ b/packages/boot/src/booters/service.booter.ts @@ -13,14 +13,14 @@ import {BootBindings} from '../keys'; type ServiceProviderClass = Constructor>; /** - * A class that extends BaseArtifactBooter to boot the 'DataSource' artifact type. + * A class that extends BaseArtifactBooter to boot the 'Service' 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 + * @param [bootConfig] Service Artifact Options Object */ export class ServiceBooter extends BaseArtifactBooter { serviceProviders: ServiceProviderClass[]; @@ -34,7 +34,7 @@ export class ServiceBooter extends BaseArtifactBooter { ) { super( projectRoot, - // Set DataSource Booter Options if passed in via bootConfig + // Set Service Booter Options if passed in via bootConfig Object.assign({}, ServiceDefaults, serviceConfig), ); } diff --git a/packages/boot/test/fixtures/lifecycle-observer.artifact.ts b/packages/boot/test/fixtures/lifecycle-observer.artifact.ts new file mode 100644 index 000000000000..ac575598aa2f --- /dev/null +++ b/packages/boot/test/fixtures/lifecycle-observer.artifact.ts @@ -0,0 +1,18 @@ +// 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 {LifeCycleObserver} from '@loopback/core'; + +export class MyLifeCycleObserver implements LifeCycleObserver { + status = ''; + + async start() { + this.status = 'started'; + } + + stop() { + this.status = 'stopped'; + } +} diff --git a/packages/boot/test/integration/lifecycle-observer.booter.integration.ts b/packages/boot/test/integration/lifecycle-observer.booter.integration.ts new file mode 100644 index 000000000000..6944084765af --- /dev/null +++ b/packages/boot/test/integration/lifecycle-observer.booter.integration.ts @@ -0,0 +1,53 @@ +// 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 { + BindingScope, + ContextTags, + CoreBindings, + CoreTags, +} from '@loopback/core'; +import {expect, TestSandbox} from '@loopback/testlab'; +import {resolve} from 'path'; +import {BooterApp} from '../fixtures/application'; + +describe('lifecycle script booter integration tests', () => { + const SANDBOX_PATH = resolve(__dirname, '../../.sandbox'); + const sandbox = new TestSandbox(SANDBOX_PATH); + + const OBSERVER_PREFIX = CoreBindings.LIFE_CYCLE_OBSERVERS; + const OBSERVER_TAG = CoreTags.LIFE_CYCLE_OBSERVER; + + let app: BooterApp; + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(getApp); + + it('boots life cycle observers when app.boot() is called', async () => { + const expectedBinding = { + key: `${OBSERVER_PREFIX}.MyLifeCycleObserver`, + tags: [ContextTags.TYPE, OBSERVER_TAG], + scope: BindingScope.SINGLETON, + }; + + await app.boot(); + + const bindings = app + .findByTag(OBSERVER_TAG) + .map(b => ({key: b.key, tags: b.tagNames, scope: b.scope})); + expect(bindings).to.containEql(expectedBinding); + }); + + async function getApp() { + await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js')); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/lifecycle-observer.artifact.js'), + 'observers/lifecycle-observer.observer.js', + ); + + const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp; + app = new MyApp(); + } +});