From 8e4d1ac77960843434762e921dfed41d01a87580 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 27 Nov 2018 12:15:26 -0800 Subject: [PATCH] feat(boot): add a booter for life cycle scripts See https://github.com/strongloop/loopback-next/issues/2034 --- packages/boot/src/boot.component.ts | 12 ++-- packages/boot/src/booters/index.ts | 1 + .../src/booters/lifecyle-script.booter.ts | 70 +++++++++++++++++++ packages/boot/src/booters/service.booter.ts | 6 +- .../fixtures/lifecycle-script.artifact.ts | 18 +++++ .../lifecycle-script.booter.integration.ts | 48 +++++++++++++ 6 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 packages/boot/src/booters/lifecyle-script.booter.ts create mode 100644 packages/boot/test/fixtures/lifecycle-script.artifact.ts create mode 100644 packages/boot/test/integration/lifecycle-script.booter.integration.ts diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts index e5474f0eace0..6418f865373e 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, + LifeCycleScriptBooter, } from './booters'; +import {Bootstrapper} from './bootstrapper'; import {BootBindings} from './keys'; /** @@ -29,6 +30,7 @@ export class BootComponent implements Component { RepositoryBooter, ServiceBooter, DataSourceBooter, + LifeCycleScriptBooter, ]; /** diff --git a/packages/boot/src/booters/index.ts b/packages/boot/src/booters/index.ts index 6bb4f8676110..ee701c20e57b 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-script.booter'; diff --git a/packages/boot/src/booters/lifecyle-script.booter.ts b/packages/boot/src/booters/lifecyle-script.booter.ts new file mode 100644 index 000000000000..e0d57e5f3766 --- /dev/null +++ b/packages/boot/src/booters/lifecyle-script.booter.ts @@ -0,0 +1,70 @@ +// 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} from '@loopback/context'; +import { + Application, + asLifeCycleObserverBinding, + CoreBindings, + isLifeCycleObserverClass, + LifeCycleObserver, +} from '@loopback/core'; +import {ArtifactOptions} from '../interfaces'; +import {BootBindings} from '../keys'; +import {BaseArtifactBooter} from './base-artifact.booter'; + +type LifeCycleObserverClass = Constructor; + +/** + * A class that extends BaseArtifactBooter to boot the 'LifeCycleScript' 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] LifeCycleScript Artifact Options Object + */ +export class LifeCycleScriptBooter extends BaseArtifactBooter { + observers: LifeCycleObserverClass[]; + + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: Application, + @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(`${BootBindings.BOOT_OPTIONS}#scripts`) + public scriptConfig: ArtifactOptions = {}, + ) { + super( + projectRoot, + // Set LifeCycleScript Booter Options if passed in via bootConfig + Object.assign({}, LifeCycleScriptDefaults, scriptConfig), + ); + } + + /** + * 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) { + this.app + .bind(`lifeCycleObservers.${observer.name}`) + .toClass(observer) + .apply(asLifeCycleObserverBinding); + } + } +} + +/** + * Default ArtifactOptions for DataSourceBooter. + */ +export const LifeCycleScriptDefaults: ArtifactOptions = { + dirs: ['scripts'], + extensions: ['.script.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-script.artifact.ts b/packages/boot/test/fixtures/lifecycle-script.artifact.ts new file mode 100644 index 000000000000..ac575598aa2f --- /dev/null +++ b/packages/boot/test/fixtures/lifecycle-script.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-script.booter.integration.ts b/packages/boot/test/integration/lifecycle-script.booter.integration.ts new file mode 100644 index 000000000000..793a4bdc09bd --- /dev/null +++ b/packages/boot/test/integration/lifecycle-script.booter.integration.ts @@ -0,0 +1,48 @@ +// 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'; +import {CoreBindings, BindingScope} from '@loopback/core'; + +describe('lifecycle script booter integration tests', () => { + const SANDBOX_PATH = resolve(__dirname, '../../.sandbox'); + const sandbox = new TestSandbox(SANDBOX_PATH); + + const SCRIPTS_PREFIX = 'lifeCycleObservers'; + const SCRIPTS_TAG = CoreBindings.LIFE_CYCLE_OBSERVER_TAG; + + let app: BooterApp; + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(getApp); + + it('boots scripts when app.boot() is called', async () => { + const expectedBinding = { + key: `${SCRIPTS_PREFIX}.MyLifeCycleObserver`, + tags: [SCRIPTS_TAG], + scope: BindingScope.SINGLETON, + }; + + await app.boot(); + + const bindings = app + .findByTag(SCRIPTS_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-script.artifact.js'), + 'scripts/lifecycle-script.script.js', + ); + + const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp; + app = new MyApp(); + } +});