Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import {Application, Component} from '@loopback/core';
import {expect, givenHttpServerConfig, TestSandbox} from '@loopback/testlab';
import {resolve} from 'path';
import {
BootBindings,
BootMixin,
createComponentApplicationBooterBinding,
} from '../..';
import {BootMixin, createComponentApplicationBooterBinding} from '../..';
import {bindingKeysExcludedFromSubApp} from '../../booters';
import {Bootable} from '../../types';
import {BooterApp} from '../fixtures/application';

describe('component application booter acceptance tests', () => {
Expand All @@ -31,29 +29,12 @@ describe('component application booter acceptance tests', () => {

const mainApp = new MainApp();
mainApp.component(BooterAppComponent);
const appBindingsBeforeBoot = mainApp.find(
// Exclude boot related bindings
binding =>
![
BootBindings.BOOT_OPTIONS.key,
BootBindings.PROJECT_ROOT.key,
BootBindings.BOOTSTRAPPER_KEY.key,
].includes(binding.key),
);
await mainApp.boot();
const controllers = mainApp.find('controllers.*').map(b => b.key);
expect(controllers).to.eql([
'controllers.ArtifactOne',
'controllers.ArtifactTwo',
]);
await testSubAppBoot(mainApp);
});

// Assert main app bindings before boot are not overridden
const appBindingsAfterBoot = mainApp.find(binding =>
appBindingsBeforeBoot.includes(binding),
);
expect(appBindingsAfterBoot.map(b => b.key)).to.eql(
appBindingsBeforeBoot.map(b => b.key),
);
it('binds artifacts booted from the sub application', async () => {
const mainApp = new MainAppWithSubAppBooter();
await testSubAppBoot(mainApp);
});

it('binds artifacts booted from the component application by filter', async () => {
Expand Down Expand Up @@ -97,6 +78,14 @@ describe('component application booter acceptance tests', () => {
}
}

class MainAppWithSubAppBooter extends BootMixin(Application) {
constructor() {
super();
this.projectRoot = __dirname;
this.applicationBooter(app);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the method name start with a verb, e.g. this.addApplicationBooter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I choose a noun to be consistent with other methods.

}
}

async function getApp() {
await sandbox.copyFile(resolve(__dirname, '../fixtures/package.json'));
await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js'));
Expand All @@ -110,4 +99,25 @@ describe('component application booter acceptance tests', () => {
rest: givenHttpServerConfig(),
});
}

async function testSubAppBoot(mainApp: Application & Bootable) {
const appBindingsBeforeBoot = mainApp.find(
// Exclude boot related bindings
binding => !bindingKeysExcludedFromSubApp.includes(binding.key),
);
await mainApp.boot();
const controllers = mainApp.find('controllers.*').map(b => b.key);
expect(controllers).to.eql([
'controllers.ArtifactOne',
'controllers.ArtifactTwo',
]);

// Assert main app bindings before boot are not overridden
const appBindingsAfterBoot = mainApp.find(binding =>
appBindingsBeforeBoot.includes(binding),
);
expect(appBindingsAfterBoot.map(b => b.key)).to.eql(
appBindingsBeforeBoot.map(b => b.key),
);
}
});
22 changes: 16 additions & 6 deletions packages/boot/src/booters/component-application.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ import {Bootable, Booter, booter} from '../types';

const debug = debugFactory('loopback:boot:booter:component-application');

/**
* Binding keys excluded from a sub application. These bindings booted from the
* sub application won't be added to the main application.
*/
export const bindingKeysExcludedFromSubApp = [
BootBindings.BOOT_OPTIONS.key,
BootBindings.PROJECT_ROOT.key,
BootBindings.BOOTSTRAPPER_KEY.key,
CoreBindings.APPLICATION_CONFIG.key,
CoreBindings.APPLICATION_INSTANCE.key,
CoreBindings.APPLICATION_METADATA.key,
CoreBindings.LIFE_CYCLE_OBSERVER_REGISTRY.key,
CoreBindings.LIFE_CYCLE_OBSERVER_OPTIONS.key,
];

/**
* Create a booter that boots the component application. Bindings that exist
* in the component application before `boot` are skipped. Locked bindings in
Expand All @@ -40,11 +55,6 @@ export function createBooterForComponentApplication(
) {}

async load() {
const bootBindingKeys = [
BootBindings.BOOT_OPTIONS.key,
BootBindings.PROJECT_ROOT.key,
BootBindings.BOOTSTRAPPER_KEY.key,
];
/**
* List all bindings before boot
*/
Expand All @@ -58,7 +68,7 @@ export function createBooterForComponentApplication(
bindings = componentApp.find(filter);
for (const binding of bindings) {
// Exclude boot related bindings
if (bootBindingKeys.includes(binding.key)) continue;
if (bindingKeysExcludedFromSubApp.includes(binding.key)) continue;

// Exclude bindings from the app before boot
if (bindingsBeforeBoot.has(binding)) {
Expand Down
11 changes: 7 additions & 4 deletions packages/boot/src/booters/model.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
// License text available at https://opensource.org/licenses/MIT

import {config, Constructor, inject} from '@loopback/context';
import {Application, CoreBindings} from '@loopback/core';
import {ModelMetadataHelper} from '@loopback/repository';
import {CoreBindings} from '@loopback/core';
import {
ApplicationWithRepositories,
ModelMetadataHelper,
} from '@loopback/repository';
import debugFactory from 'debug';
import {BootBindings} from '../keys';
import {ArtifactOptions, booter} from '../types';
Expand All @@ -26,7 +29,7 @@ const debug = debugFactory('loopback:boot:model-booter');
export class ModelBooter extends BaseArtifactBooter {
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
public app: Application,
public app: ApplicationWithRepositories,
@inject(BootBindings.PROJECT_ROOT) projectRoot: string,
@config()
public modelConfig: ArtifactOptions = {},
Expand All @@ -53,7 +56,7 @@ export class ModelBooter extends BaseArtifactBooter {

debug('Bind class: %s', cls.name);
// We are binding the model class itself
const binding = this.app.bind(`models.${cls.name}`).to(cls).tag('model');
const binding = this.app.model(cls);
debug('Binding created for model class %s: %j', cls.name, binding);
}
}
Expand Down
16 changes: 16 additions & 0 deletions packages/boot/src/mixins/boot.mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import {
Binding,
BindingFilter,
BindingFromClassOptions,
BindingScope,
Constructor,
Expand All @@ -13,6 +14,7 @@ import {
} from '@loopback/context';
import {Application, Component, MixinTarget} from '@loopback/core';
import {BootComponent} from '../boot.component';
import {createComponentApplicationBooterBinding} from '../booters/component-application.booter';
import {Bootstrapper} from '../bootstrapper';
import {BootBindings, BootTags} from '../keys';
import {Bootable, Booter, BootOptions} from '../types';
Expand Down Expand Up @@ -104,6 +106,20 @@ export function BootMixin<T extends MixinTarget<Application>>(superClass: T) {
);
}

/**
* Register a booter to boot a sub-application. See
* {@link createComponentApplicationBooterBinding} for more details.
*
* @param subApp - A sub-application with artifacts to be booted
* @param filter - A binding filter to select what bindings from the sub
* application should be added to the main application.
*/
applicationBooter(subApp: Application & Bootable, filter?: BindingFilter) {
const binding = createComponentApplicationBooterBinding(subApp, filter);
this.add(binding);
return binding;
}

/**
* Override to ensure any Booter's on a Component are also mounted.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Repository,
RepositoryMixin,
} from '../../..';
import {model, property} from '../../../decorators';

describe('RepositoryMixin', () => {
it('mixed class has .repository()', () => {
Expand Down Expand Up @@ -212,6 +213,7 @@ describe('RepositoryMixin', () => {
expect(boundRepositories).to.containEql('repositories.NoteRepo');
const binding = myApp.getBinding('repositories.NoteRepo');
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
expect(binding.tagMap).to.have.property('repository');
const repoInstance = myApp.getSync('repositories.NoteRepo');
expect(repoInstance).to.be.instanceOf(NoteRepo);
}
Expand Down Expand Up @@ -247,7 +249,8 @@ describe('RepositoryMixin dataSource', () => {
it('binds dataSource class using the dataSourceName property', () => {
const myApp = new AppWithRepoMixin();

myApp.dataSource(FooDataSource);
const binding = myApp.dataSource(FooDataSource);
expect(binding.tagMap).to.have.property('datasource');
expectDataSourceToBeBound(myApp, FooDataSource, 'foo');
});

Expand Down Expand Up @@ -292,6 +295,9 @@ describe('RepositoryMixin dataSource', () => {
expect(app.find('datasources.*').map(d => d.key)).to.containEql(
`datasources.${name}`,
);
expect(app.findByTag('datasource').map(d => d.key)).to.containEql(
`datasources.${name}`,
);
expect(app.getSync(`datasources.${name}`)).to.be.instanceOf(ds);
};

Expand All @@ -316,3 +322,26 @@ describe('RepositoryMixin dataSource', () => {
}
}
});

describe('RepositoryMixin model', () => {
it('mixes into the target class', () => {
const myApp = new AppWithRepoMixin();
expect(typeof myApp.model).to.be.eql('function');
});

it('binds a model class', () => {
const myApp = new AppWithRepoMixin();
const binding = myApp.model(MyModel);
expect(binding.key).to.eql('models.MyModel');
expect(binding.tagMap).to.have.property('model');
expect(myApp.getSync('models.MyModel')).to.eql(MyModel);
});

@model()
class MyModel {
@property()
name: string;
}

class AppWithRepoMixin extends RepositoryMixin(Application) {}
});
3 changes: 2 additions & 1 deletion packages/repository/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ export * from './connectors';
export * from './datasource';
export * from './decorators';
export * from './define-model-class';
export * from './define-repository-class';
export * from './errors';
export * from './keys';
export * from './mixins';
export * from './model';
export * from './query';
export * from './relations';
export * from './repositories';
export * from './define-repository-class';
export * from './transaction';
export * from './type-resolver';
export * from './types';
40 changes: 40 additions & 0 deletions packages/repository/src/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/repository
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

/**
* Binding tags for repository related bindings
*/
export namespace RepositoryTags {
/**
* Tag for model class bindings
*/
export const MODEL = 'model';
/**
* Tag for repository bindings
*/
export const REPOSITORY = 'repository';
/**
* Tag for datasource bindings
*/
export const DATASOURCE = 'datasource';
}

/**
* Binding keys and namespaces for repository related bindings
*/
export namespace RepositoryBindings {
/**
* Namespace for model class bindings
*/
export const MODELS = 'models';
/**
* Namespace for repository bindings
*/
export const REPOSITORIES = 'repositories';
/**
* Namespace for datasource bindings
*/
export const DATASOURCES = 'datasources';
}
Loading