diff --git a/packages/boot/src/__tests__/fixtures/multiple-models.model.ts b/packages/boot/src/__tests__/fixtures/multiple-models.model.ts new file mode 100644 index 000000000000..e6f47b18417c --- /dev/null +++ b/packages/boot/src/__tests__/fixtures/multiple-models.model.ts @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2020. 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 {Entity, Model} from '@loopback/repository'; + +export class Model1 extends Model {} + +export class Model2 extends Entity {} diff --git a/packages/boot/src/__tests__/integration/model.booter.integration.ts b/packages/boot/src/__tests__/integration/model.booter.integration.ts new file mode 100644 index 000000000000..0d54b3626e09 --- /dev/null +++ b/packages/boot/src/__tests__/integration/model.booter.integration.ts @@ -0,0 +1,53 @@ +// Copyright IBM Corp. 2019,2020. 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'; + +describe('repository booter integration tests', () => { + const sandbox = new TestSandbox(resolve(__dirname, '../../.sandbox')); + + const MODELS_TAG = 'model'; + + let app: BooterApp; + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(getApp); + + it('boots repositories when app.boot() is called', async () => { + const expectedBindings = [ + 'models.Model1', + 'models.Model2', + 'models.NoEntity', + 'models.Product', + ]; + + await app.boot(); + + const bindings = app.findByTag(MODELS_TAG).map(b => b.key); + expect(bindings.sort()).to.eql(expectedBindings.sort()); + }); + + async function getApp() { + await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js')); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/no-entity.model.js'), + 'models/no-entity.model.js', + ); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.copyFile( + resolve(__dirname, '../fixtures/multiple-models.model.js'), + 'models/multiple-models.model.js', + ); + + const MyApp = require(resolve(sandbox.path, 'application.js')).BooterApp; + app = new MyApp(); + } +}); diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts index 69ab4d713719..bca7fdd20543 100644 --- a/packages/boot/src/boot.component.ts +++ b/packages/boot/src/boot.component.ts @@ -12,6 +12,7 @@ import { InterceptorProviderBooter, LifeCycleObserverBooter, ModelApiBooter, + ModelBooter, RepositoryBooter, ServiceBooter, } from './booters'; @@ -35,6 +36,7 @@ export class BootComponent implements Component { LifeCycleObserverBooter, InterceptorProviderBooter, ModelApiBooter, + ModelBooter, ]; /** diff --git a/packages/boot/src/booters/datasource.booter.ts b/packages/boot/src/booters/datasource.booter.ts index 77704b328e28..d30d320f3de8 100644 --- a/packages/boot/src/booters/datasource.booter.ts +++ b/packages/boot/src/booters/datasource.booter.ts @@ -16,7 +16,7 @@ import {BaseArtifactBooter} from './base-artifact.booter'; /** * A class that extends BaseArtifactBooter to boot the 'DataSource' artifact type. - * Discovered DataSources are bound using `app.controller()`. + * Discovered DataSources are bound using `app.dataSource()`. * * Supported phases: configure, discover, load * diff --git a/packages/boot/src/booters/index.ts b/packages/boot/src/booters/index.ts index 4fe98be2e9de..a3fa2e6350db 100644 --- a/packages/boot/src/booters/index.ts +++ b/packages/boot/src/booters/index.ts @@ -12,5 +12,6 @@ export * from './datasource.booter'; export * from './interceptor.booter'; export * from './lifecyle-observer.booter'; export * from './model-api.booter'; +export * from './model.booter'; export * from './repository.booter'; export * from './service.booter'; diff --git a/packages/boot/src/booters/model.booter.ts b/packages/boot/src/booters/model.booter.ts new file mode 100644 index 000000000000..37dc8fbca926 --- /dev/null +++ b/packages/boot/src/booters/model.booter.ts @@ -0,0 +1,73 @@ +// Copyright IBM Corp. 2020. 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 {config, Constructor, inject} from '@loopback/context'; +import {Application, CoreBindings} from '@loopback/core'; +import {ModelMetadataHelper} from '@loopback/repository'; +import debugFactory from 'debug'; +import {BootBindings} from '../keys'; +import {ArtifactOptions, booter} from '../types'; +import {BaseArtifactBooter} from './base-artifact.booter'; + +const debug = debugFactory('loopback:boot:model-booter'); + +/** + * A class that extends BaseArtifactBooter to boot the 'Model' 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 - Model Artifact Options Object + */ +@booter('models') +export class ModelBooter extends BaseArtifactBooter { + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: Application, + @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @config() + public modelConfig: ArtifactOptions = {}, + ) { + super( + projectRoot, + // Set Model Booter Options if passed in via bootConfig + Object.assign({}, ModelDefaults, modelConfig), + ); + } + + /** + * 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(); + + for (const cls of this.classes) { + if (!isModelClass(cls)) { + debug('Skipping class %s - no @model is found', cls.name); + continue; + } + + 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'); + debug('Binding created for model class %s: %j', cls.name, binding); + } + } +} + +/** + * Default ArtifactOptions for DataSourceBooter. + */ +export const ModelDefaults: ArtifactOptions = { + dirs: ['models'], + extensions: ['.model.js'], + nested: true, +}; + +function isModelClass(cls: Constructor) { + return ModelMetadataHelper.getModelMetadata(cls) != null; +} diff --git a/packages/boot/src/booters/service.booter.ts b/packages/boot/src/booters/service.booter.ts index e431d979fcd2..bd0efb38b1ca 100644 --- a/packages/boot/src/booters/service.booter.ts +++ b/packages/boot/src/booters/service.booter.ts @@ -21,7 +21,7 @@ const debug = debugFactory('loopback:boot:service-booter'); /** * A class that extends BaseArtifactBooter to boot the 'Service' artifact type. - * Discovered DataSources are bound using `app.controller()`. + * Discovered services are bound using `app.service()`. * * Supported phases: configure, discover, load *