Skip to content

Commit

Permalink
feat(boot): add a booter for life cycle scripts
Browse files Browse the repository at this point in the history
See #2034
  • Loading branch information
raymondfeng committed Apr 9, 2019
1 parent 27c8127 commit 6912f76
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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';

/**
* An mock-up `LifeCycleObserver`. Please note that `start` and `stop` methods
* can be async or sync.
*/
export class MyLifeCycleObserver implements LifeCycleObserver {
status = '';

/**
* Handling `start` event asynchronously
*/
async start() {
// Perform some work asynchronously
// await startSomeAsyncWork(...)
this.status = 'started';
}

/**
* Handling `stop` event synchronously.
*/
stop() {
this.status = 'stopped';
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
});
12 changes: 7 additions & 5 deletions packages/boot/src/boot.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -29,6 +30,7 @@ export class BootComponent implements Component {
RepositoryBooter,
ServiceBooter,
DataSourceBooter,
LifeCycleObserverBooter,
];

/**
Expand Down
1 change: 1 addition & 0 deletions packages/boot/src/booters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
71 changes: 71 additions & 0 deletions packages/boot/src/booters/lifecyle-observer.booter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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,
CoreBindings,
isLifeCycleObserverClass,
LifeCycleObserver,
} from '@loopback/core';
import * as debugFactory from 'debug';
import {ArtifactOptions} from '../interfaces';
import {BootBindings} from '../keys';
import {BaseArtifactBooter} from './base-artifact.booter';

const debug = debugFactory('loopback:boot:lifecycle-observer-booter');

type LifeCycleObserverClass = Constructor<LifeCycleObserver>;

/**
* 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,
};
6 changes: 3 additions & 3 deletions packages/boot/src/booters/service.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import {BootBindings} from '../keys';
type ServiceProviderClass = Constructor<Provider<object>>;

/**
* 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[];
Expand All @@ -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),
);
}
Expand Down

0 comments on commit 6912f76

Please sign in to comment.