From d8c116b12a688e7b1a8b16603967615b5e39cf51 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 31 May 2020 13:24:44 -0700 Subject: [PATCH 1/8] feat(boot): move non-core booters out of `@loopback/boot` Signed-off-by: Raymond Feng BREAKING CHANGE: Model/DataSource/Repository/ModelApi booters are moved to `@loopback/repository` and `@loopback/model-api-builder` packages. There are no breaking changes for other APIs. Existing applications should continue to work unless they use such booters explicitly. --- packages/boot/package.json | 5 - .../controller.booter.acceptance.ts | 23 +-- .../crud-rest.api-builder.acceptance.ts | 170 ----------------- .../acceptance/model-api.booter.acceptance.ts | 172 ------------------ .../src/__tests__/fixtures/application.ts | 13 +- .../fixtures/bindable-classes.artifact.ts | 2 +- .../__tests__/fixtures/datasource.artifact.ts | 14 -- .../fixtures/interceptor.artifact.ts | 2 +- .../fixtures/lifecycle-observer.artifact.ts | 2 +- .../fixtures/multiple-models.model.ts | 10 - .../__tests__/fixtures/multiple.artifact.ts | 4 - .../src/__tests__/fixtures/no-entity.model.ts | 15 -- .../non-global-interceptor.artifact.ts | 2 +- .../src/__tests__/fixtures/product.model.ts | 15 -- .../__tests__/fixtures/product.repository.ts | 17 -- .../fixtures/service-class.artifact.ts | 2 +- .../fixtures/service-provider.artifact.ts | 2 +- .../fixtures/stub-model-api-builder.ts | 67 ------- .../datasource.booter.integration.ts | 40 ---- .../integration/model.booter.integration.ts | 53 ------ .../repository.booter.integration.ts | 44 ----- .../src/__tests__/unit/boot.component.unit.ts | 16 -- .../boot.custom-binding.component.unit.ts | 2 +- .../unit/booters/datasource.booter.unit.ts | 100 ---------- .../unit/booters/repository.booter.unit.ts | 105 ----------- .../unit/booters/service.booter.unit.ts | 8 +- .../src/__tests__/unit/bootstrapper.unit.ts | 7 +- .../__tests__/unit/mixins/boot.mixin.unit.ts | 2 +- packages/boot/src/boot.component.ts | 37 ++-- packages/boot/src/booters/booter-utils.ts | 2 +- .../boot/src/booters/datasource.booter.ts | 76 -------- packages/boot/src/booters/index.ts | 4 - packages/boot/src/booters/model-api.booter.ts | 117 ------------ packages/boot/src/booters/model.booter.ts | 75 -------- .../boot/src/booters/repository.booter.ts | 72 -------- packages/boot/src/booters/service.booter.ts | 4 +- packages/boot/src/bootstrapper.ts | 14 +- packages/boot/src/keys.ts | 17 +- packages/boot/src/mixins/boot.mixin.ts | 2 +- packages/boot/tsconfig.json | 15 -- 40 files changed, 56 insertions(+), 1293 deletions(-) delete mode 100644 packages/boot/src/__tests__/acceptance/crud-rest.api-builder.acceptance.ts delete mode 100644 packages/boot/src/__tests__/acceptance/model-api.booter.acceptance.ts delete mode 100644 packages/boot/src/__tests__/fixtures/datasource.artifact.ts delete mode 100644 packages/boot/src/__tests__/fixtures/multiple-models.model.ts delete mode 100644 packages/boot/src/__tests__/fixtures/no-entity.model.ts delete mode 100644 packages/boot/src/__tests__/fixtures/product.model.ts delete mode 100644 packages/boot/src/__tests__/fixtures/product.repository.ts delete mode 100644 packages/boot/src/__tests__/fixtures/stub-model-api-builder.ts delete mode 100644 packages/boot/src/__tests__/integration/datasource.booter.integration.ts delete mode 100644 packages/boot/src/__tests__/integration/model.booter.integration.ts delete mode 100644 packages/boot/src/__tests__/integration/repository.booter.integration.ts delete mode 100644 packages/boot/src/__tests__/unit/booters/datasource.booter.unit.ts delete mode 100644 packages/boot/src/__tests__/unit/booters/repository.booter.unit.ts delete mode 100644 packages/boot/src/booters/datasource.booter.ts delete mode 100644 packages/boot/src/booters/model-api.booter.ts delete mode 100644 packages/boot/src/booters/model.booter.ts delete mode 100644 packages/boot/src/booters/repository.booter.ts diff --git a/packages/boot/package.json b/packages/boot/package.json index 0ad69f86d54b..2c39cd26d1df 100644 --- a/packages/boot/package.json +++ b/packages/boot/package.json @@ -27,9 +27,6 @@ "@loopback/core": "^2.11.0" }, "dependencies": { - "@loopback/model-api-builder": "^2.1.16", - "@loopback/repository": "^3.1.0", - "@loopback/service-proxy": "^3.0.2", "@types/debug": "^4.1.5", "@types/glob": "^7.1.3", "debug": "^4.2.0", @@ -40,8 +37,6 @@ "@loopback/build": "^6.2.5", "@loopback/core": "^2.11.0", "@loopback/eslint-config": "^10.0.1", - "@loopback/rest": "^8.0.0", - "@loopback/rest-crud": "^0.8.16", "@loopback/testlab": "^3.2.7", "@types/node": "^10.17.35" }, diff --git a/packages/boot/src/__tests__/acceptance/controller.booter.acceptance.ts b/packages/boot/src/__tests__/acceptance/controller.booter.acceptance.ts index dc8d2258164d..372ac2b64201 100644 --- a/packages/boot/src/__tests__/acceptance/controller.booter.acceptance.ts +++ b/packages/boot/src/__tests__/acceptance/controller.booter.acceptance.ts @@ -3,11 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - createRestAppClient, - givenHttpServerConfig, - TestSandbox, -} from '@loopback/testlab'; +import {expect, givenHttpServerConfig, TestSandbox} from '@loopback/testlab'; import {resolve} from 'path'; import {BooterApp} from '../fixtures/application'; @@ -18,17 +14,10 @@ describe('controller booter acceptance tests', () => { beforeEach('reset sandbox', () => sandbox.reset()); beforeEach(getApp); - afterEach(stopApp); - - it('binds controllers using ControllerDefaults and REST endpoints work', async () => { + it('binds controllers using ControllerDefaults', async () => { await app.boot(); - await app.start(); - - const client = createRestAppClient(app); - - // Default Controllers = /controllers with .controller.js ending (nested = true); - await client.get('/one').expect(200, 'ControllerOne.one()'); - await client.get('/two').expect(200, 'ControllerTwo.two()'); + const bindings = app.find('controllers.*'); + expect(bindings.length).to.eql(2); }); async function getApp() { @@ -44,8 +33,4 @@ describe('controller booter acceptance tests', () => { rest: givenHttpServerConfig(), }); } - - async function stopApp() { - await app?.stop(); - } }); diff --git a/packages/boot/src/__tests__/acceptance/crud-rest.api-builder.acceptance.ts b/packages/boot/src/__tests__/acceptance/crud-rest.api-builder.acceptance.ts deleted file mode 100644 index 5cee86b9b78b..000000000000 --- a/packages/boot/src/__tests__/acceptance/crud-rest.api-builder.acceptance.ts +++ /dev/null @@ -1,170 +0,0 @@ -// 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 {ApplicationConfig} from '@loopback/core'; -import {juggler, RepositoryMixin} from '@loopback/repository'; -import {RestApplication} from '@loopback/rest'; -import {CrudRestComponent} from '@loopback/rest-crud'; -import {expect, givenHttpServerConfig, TestSandbox} from '@loopback/testlab'; -import {resolve} from 'path'; -import {BootMixin, ModelApiBooter} from '../..'; -import {ProductRepository} from '../fixtures/product.repository'; - -describe('CRUD rest builder acceptance tests', () => { - let app: BooterApp; - const sandbox = new TestSandbox(resolve(__dirname, '../../.sandbox')); - - beforeEach('reset sandbox', () => sandbox.reset()); - beforeEach(givenAppWithDataSource); - - afterEach(stopApp); - - it('binds the controller and repository to the application', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/product.model.js'), - 'models/product.model.js', - ); - - // when creating the config file in a real app, make sure to use - // module.exports = {...} - // it's not used here because this is a .js file - await sandbox.writeTextFile( - 'model-endpoints/product.rest-config.js', - ` -const {Product} = require('../models/product.model'); -module.exports = { - model: Product, - pattern: 'CrudRest', - dataSource: 'db', - basePath: '/products', -}; - `, - ); - - // Boot & start the application - await app.boot(); - await app.start(); - - expect(app.getBinding('repositories.ProductRepository').key).to.eql( - 'repositories.ProductRepository', - ); - - expect(app.getBinding('controllers.ProductController').key).to.eql( - 'controllers.ProductController', - ); - }); - - it('uses bound repository class if it exists', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/product.model.js'), - 'models/product.model.js', - ); - - await sandbox.writeTextFile( - 'model-endpoints/product.rest-config.js', - ` -const {Product} = require('../models/product.model'); -module.exports = { - model: Product, - pattern: 'CrudRest', - dataSource: 'db', - basePath: '/products', -}; - `, - ); - - app.repository(ProductRepository); - - const bindingName = 'repositories.ProductRepository'; - - const binding = app.getBinding(bindingName); - expect(binding.valueConstructor).to.eql(ProductRepository); - - // Boot & start the application - await app.boot(); - await app.start(); - - // Make sure it is still equal to the defined ProductRepository after - // booting - expect(app.getBinding(bindingName).valueConstructor).to.eql( - ProductRepository, - ); - - expect(app.getBinding('controllers.ProductController').key).to.eql( - 'controllers.ProductController', - ); - }); - - it('throws if there is no base path in the config', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/product.model.js'), - 'models/product.model.js', - ); - - await sandbox.writeTextFile( - 'model-endpoints/product.rest-config.js', - ` -const {Product} = require('../models/product.model'); -module.exports = { - model: Product, - pattern: 'CrudRest', - dataSource: 'db', - // basePath not specified -}; - `, - ); - - // Boot the application - await expect(app.boot()).to.be.rejectedWith( - /Missing required field "basePath" in configuration for model Product./, - ); - }); - - it('throws if a Model is used instead of an Entity', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/no-entity.model.js'), - 'models/no-entity.model.js', - ); - - await sandbox.writeTextFile( - 'model-endpoints/no-entity.rest-config.js', - ` -const {NoEntity} = require('../models/no-entity.model'); -module.exports = { - // this model extends Model, not Entity - model: NoEntity, - pattern: 'CrudRest', - dataSource: 'db', - basePath: '/no-entities', -}; - `, - ); - - // Boot the application - await expect(app.boot()).to.be.rejectedWith( - /CrudRestController requires a model that extends 'Entity'./, - ); - }); - - class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) { - constructor(options?: ApplicationConfig) { - super(options); - this.projectRoot = sandbox.path; - this.booters(ModelApiBooter); - this.component(CrudRestComponent); - } - } - - async function givenAppWithDataSource() { - app = new BooterApp({ - rest: givenHttpServerConfig(), - }); - app.dataSource(new juggler.DataSource({connector: 'memory'}), 'db'); - } - - async function stopApp() { - if (app?.state === 'started') await app?.stop(); - } -}); diff --git a/packages/boot/src/__tests__/acceptance/model-api.booter.acceptance.ts b/packages/boot/src/__tests__/acceptance/model-api.booter.acceptance.ts deleted file mode 100644 index c610e517fbcd..000000000000 --- a/packages/boot/src/__tests__/acceptance/model-api.booter.acceptance.ts +++ /dev/null @@ -1,172 +0,0 @@ -// 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 {ApplicationConfig} from '@loopback/core'; -import {juggler, RepositoryMixin} from '@loopback/repository'; -import {RestApplication} from '@loopback/rest'; -import { - expect, - givenHttpServerConfig, - TestSandbox, - toJSON, -} from '@loopback/testlab'; -import {resolve} from 'path'; -import {BootMixin, ModelApiBooter} from '../..'; -import {Product} from '../fixtures/product.model'; -import { - buildCalls, - samePatternBuildCalls, - SamePatternModelApiBuilderComponent, - similarPatternBuildCalls, - SimilarPatternModelApiBuilderComponent, - StubModelApiBuilderComponent, -} from '../fixtures/stub-model-api-builder'; - -describe('model API booter acceptance tests', () => { - let app: BooterApp; - const sandbox = new TestSandbox(resolve(__dirname, '../../.sandbox')); - - beforeEach('reset sandbox', () => sandbox.reset()); - beforeEach(givenAppWithDataSource); - - afterEach(stopApp); - - it('uses the correct model API builder', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/product.model.js'), - 'models/product.model.js', - ); - - await sandbox.writeTextFile( - 'model-endpoints/product.rest-config.js', - ` -const {Product} = require('../models/product.model'); -module.exports = { - model: Product, - pattern: 'stub', - dataSource: 'db', - basePath: '/products', -}; - `, - ); - - // Boot & start the application - await app.boot(); - await app.start(); - - expect(toJSON(buildCalls)).to.deepEqual( - toJSON([ - { - application: app, - modelClass: Product, - config: { - basePath: '/products', - dataSource: 'db', - pattern: 'stub', - }, - }, - ]), - ); - }); - - it('uses the API builder registered first if there is a duplicate pattern name', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/product.model.js'), - 'models/product.model.js', - ); - - await sandbox.writeTextFile( - 'model-endpoints/product.rest-config.js', - ` -const {Product} = require('../models/product.model'); -module.exports = { - model: Product, - pattern: 'same', - dataSource: 'db', - basePath: '/products', -}; - `, - ); - - // Boot & start the application - await app.boot(); - await app.start(); - - // registered first - expect(toJSON(samePatternBuildCalls)).to.eql([toJSON(app)]); - - expect(similarPatternBuildCalls).to.be.empty(); - }); - - it('throws if there are no patterns matching', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/product.model.js'), - 'models/product.model.js', - ); - - await sandbox.writeTextFile( - 'model-endpoints/product.rest-config.js', - ` -const {Product} = require('../models/product.model'); -module.exports = { - model: Product, - pattern: 'doesntExist', - dataSource: 'db', - basePath: '/products', -}; - `, - ); - - await expect(app.boot()).to.be.rejectedWith( - /Unsupported API pattern "doesntExist"/, - ); - }); - - it('throws if the model class is invalid', async () => { - await sandbox.copyFile( - resolve(__dirname, '../fixtures/product.model.js'), - 'models/product.model.js', - ); - - await sandbox.writeTextFile( - 'model-endpoints/product.rest-config.js', - ` -const Product = 'product' -module.exports = { - model: Product, - pattern: 'stub', - dataSource: 'db', - basePath: '/products', -}; - `, - ); - - await expect(app.boot()).to.be.rejectedWith( - /Invalid "model" field\. Expected a Model class, found product/, - ); - }); - - class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) { - constructor(options?: ApplicationConfig) { - super(options); - this.projectRoot = sandbox.path; - this.booters(ModelApiBooter); - this.component(StubModelApiBuilderComponent); - this.component(SamePatternModelApiBuilderComponent); - this.component(SimilarPatternModelApiBuilderComponent); - } - } - - async function givenAppWithDataSource() { - app = new BooterApp({ - rest: givenHttpServerConfig(), - }); - app.dataSource(new juggler.DataSource({connector: 'memory'}), 'db'); - } - - async function stopApp() { - if (app?.state === 'started') await app?.stop(); - } -}); diff --git a/packages/boot/src/__tests__/fixtures/application.ts b/packages/boot/src/__tests__/fixtures/application.ts index 7ad7acc0e148..dd0eae529120 100644 --- a/packages/boot/src/__tests__/fixtures/application.ts +++ b/packages/boot/src/__tests__/fixtures/application.ts @@ -3,19 +3,10 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ApplicationConfig} from '@loopback/core'; -import {RepositoryMixin} from '@loopback/repository'; -import {RestApplication} from '@loopback/rest'; -import {ServiceMixin} from '@loopback/service-proxy'; +import {Application, ApplicationConfig} from '@loopback/core'; import {BootMixin} from '../..'; -// Force package.json to be copied to `dist` by `tsc` -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import * as pkg from './package.json'; - -export class BooterApp extends BootMixin( - ServiceMixin(RepositoryMixin(RestApplication)), -) { +export class BooterApp extends BootMixin(Application) { constructor(options?: ApplicationConfig) { super(options); this.projectRoot = __dirname; diff --git a/packages/boot/src/__tests__/fixtures/bindable-classes.artifact.ts b/packages/boot/src/__tests__/fixtures/bindable-classes.artifact.ts index a9c5b922f942..da61d1415aeb 100644 --- a/packages/boot/src/__tests__/fixtures/bindable-classes.artifact.ts +++ b/packages/boot/src/__tests__/fixtures/bindable-classes.artifact.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 diff --git a/packages/boot/src/__tests__/fixtures/datasource.artifact.ts b/packages/boot/src/__tests__/fixtures/datasource.artifact.ts deleted file mode 100644 index 5390b4b66950..000000000000 --- a/packages/boot/src/__tests__/fixtures/datasource.artifact.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright IBM Corp. 2019. 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 {juggler} from '@loopback/repository'; - -export class DbDataSource extends juggler.DataSource { - static dataSourceName = 'db'; - - constructor() { - super({name: 'db'}); - } -} diff --git a/packages/boot/src/__tests__/fixtures/interceptor.artifact.ts b/packages/boot/src/__tests__/fixtures/interceptor.artifact.ts index 4d871b1644ce..9c01657ed514 100644 --- a/packages/boot/src/__tests__/fixtures/interceptor.artifact.ts +++ b/packages/boot/src/__tests__/fixtures/interceptor.artifact.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 diff --git a/packages/boot/src/__tests__/fixtures/lifecycle-observer.artifact.ts b/packages/boot/src/__tests__/fixtures/lifecycle-observer.artifact.ts index 0927cd216958..45a93099977f 100644 --- a/packages/boot/src/__tests__/fixtures/lifecycle-observer.artifact.ts +++ b/packages/boot/src/__tests__/fixtures/lifecycle-observer.artifact.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2018. All Rights Reserved. +// Copyright IBM Corp. 2018,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 diff --git a/packages/boot/src/__tests__/fixtures/multiple-models.model.ts b/packages/boot/src/__tests__/fixtures/multiple-models.model.ts deleted file mode 100644 index e6f47b18417c..000000000000 --- a/packages/boot/src/__tests__/fixtures/multiple-models.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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__/fixtures/multiple.artifact.ts b/packages/boot/src/__tests__/fixtures/multiple.artifact.ts index a3fcab4834b2..00cc0d71e893 100644 --- a/packages/boot/src/__tests__/fixtures/multiple.artifact.ts +++ b/packages/boot/src/__tests__/fixtures/multiple.artifact.ts @@ -3,17 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {get} from '@loopback/rest'; - export class ArtifactOne { - @get('/one') one() { return 'ControllerOne.one()'; } } export class ArtifactTwo { - @get('/two') two() { return 'ControllerTwo.two()'; } diff --git a/packages/boot/src/__tests__/fixtures/no-entity.model.ts b/packages/boot/src/__tests__/fixtures/no-entity.model.ts deleted file mode 100644 index 05d310ebc22f..000000000000 --- a/packages/boot/src/__tests__/fixtures/no-entity.model.ts +++ /dev/null @@ -1,15 +0,0 @@ -// 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 {model, Model, property} from '@loopback/repository'; - -@model() -export class NoEntity extends Model { - @property({id: true}) - id: number; - - @property({required: true}) - name: string; -} diff --git a/packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts b/packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts index 0300cf35d859..ec8990e2618d 100644 --- a/packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts +++ b/packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 diff --git a/packages/boot/src/__tests__/fixtures/product.model.ts b/packages/boot/src/__tests__/fixtures/product.model.ts deleted file mode 100644 index fe11f7cd037c..000000000000 --- a/packages/boot/src/__tests__/fixtures/product.model.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright IBM Corp. 2019. 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, property} from '@loopback/repository'; - -@model() -export class Product extends Entity { - @property({id: true}) - id: number; - - @property({required: true}) - name: string; -} diff --git a/packages/boot/src/__tests__/fixtures/product.repository.ts b/packages/boot/src/__tests__/fixtures/product.repository.ts deleted file mode 100644 index 471151129079..000000000000 --- a/packages/boot/src/__tests__/fixtures/product.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -// 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 {inject} from '@loopback/core'; -import {DefaultCrudRepository, juggler} from '@loopback/repository'; -import {Product} from './product.model'; - -export class ProductRepository extends DefaultCrudRepository< - Product, - typeof Product.prototype.id -> { - constructor(@inject('datasources.db') dataSource: juggler.DataSource) { - super(Product, dataSource); - } -} diff --git a/packages/boot/src/__tests__/fixtures/service-class.artifact.ts b/packages/boot/src/__tests__/fixtures/service-class.artifact.ts index c62abb898c29..9aea5cdccdb7 100644 --- a/packages/boot/src/__tests__/fixtures/service-class.artifact.ts +++ b/packages/boot/src/__tests__/fixtures/service-class.artifact.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 diff --git a/packages/boot/src/__tests__/fixtures/service-provider.artifact.ts b/packages/boot/src/__tests__/fixtures/service-provider.artifact.ts index 151ab88a957d..5bd227efd34e 100644 --- a/packages/boot/src/__tests__/fixtures/service-provider.artifact.ts +++ b/packages/boot/src/__tests__/fixtures/service-provider.artifact.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 diff --git a/packages/boot/src/__tests__/fixtures/stub-model-api-builder.ts b/packages/boot/src/__tests__/fixtures/stub-model-api-builder.ts deleted file mode 100644 index 21fbfcc58b51..000000000000 --- a/packages/boot/src/__tests__/fixtures/stub-model-api-builder.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright IBM Corp. 2019. 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 {Component, createBindingFromClass, injectable} from '@loopback/core'; -import { - asModelApiBuilder, - ModelApiBuilder, - ModelApiConfig, -} from '@loopback/model-api-builder'; -import {Model} from '@loopback/rest'; -import {BooterApp} from './application'; - -export const buildCalls: object[] = []; - -@injectable(asModelApiBuilder) -class StubModelApiBuilder implements ModelApiBuilder { - readonly pattern: string = 'stub'; - async build( - application: BooterApp, - modelClass: typeof Model & {prototype: Model}, - config: ModelApiConfig, - ): Promise { - buildCalls.push({application, modelClass, config}); - } -} - -export class StubModelApiBuilderComponent implements Component { - bindings = [createBindingFromClass(StubModelApiBuilder)]; -} - -export const samePatternBuildCalls: object[] = []; - -@injectable(asModelApiBuilder) -class SamePatternModelApiBuilder implements ModelApiBuilder { - readonly pattern: string = 'same'; - async build( - application: BooterApp, - modelClass: typeof Model & {prototype: Model}, - config: ModelApiConfig, - ): Promise { - samePatternBuildCalls.push(application); - } -} - -export class SamePatternModelApiBuilderComponent implements Component { - bindings = [createBindingFromClass(SamePatternModelApiBuilder)]; -} - -export const similarPatternBuildCalls: object[] = []; - -@injectable(asModelApiBuilder) -class SimilarPatternModelApiBuilder implements ModelApiBuilder { - readonly pattern: string = 'same'; - async build( - application: BooterApp, - modelClass: typeof Model & {prototype: Model}, - config: ModelApiConfig, - ): Promise { - similarPatternBuildCalls.push({modelClass}); - } -} - -export class SimilarPatternModelApiBuilderComponent implements Component { - bindings = [createBindingFromClass(SimilarPatternModelApiBuilder)]; -} diff --git a/packages/boot/src/__tests__/integration/datasource.booter.integration.ts b/packages/boot/src/__tests__/integration/datasource.booter.integration.ts deleted file mode 100644 index c7eb0d86d2cb..000000000000 --- a/packages/boot/src/__tests__/integration/datasource.booter.integration.ts +++ /dev/null @@ -1,40 +0,0 @@ -// 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('datasource booter integration tests', () => { - const sandbox = new TestSandbox(resolve(__dirname, '../../.sandbox')); - - const DATASOURCES_PREFIX = 'datasources'; - const DATASOURCES_TAG = 'datasource'; - - let app: BooterApp; - - beforeEach('reset sandbox', () => sandbox.reset()); - beforeEach(getApp); - - it('boots datasources when app.boot() is called', async () => { - const expectedBindings = [`${DATASOURCES_PREFIX}.db`]; - - await app.boot(); - - const bindings = app.findByTag(DATASOURCES_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/datasource.artifact.js'), - 'datasources/db.datasource.js', - ); - - const MyApp = require(resolve(sandbox.path, 'application.js')).BooterApp; - app = new MyApp(); - } -}); diff --git a/packages/boot/src/__tests__/integration/model.booter.integration.ts b/packages/boot/src/__tests__/integration/model.booter.integration.ts deleted file mode 100644 index a34c1d5aa6b5..000000000000 --- a/packages/boot/src/__tests__/integration/model.booter.integration.ts +++ /dev/null @@ -1,53 +0,0 @@ -// 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 {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/__tests__/integration/repository.booter.integration.ts b/packages/boot/src/__tests__/integration/repository.booter.integration.ts deleted file mode 100644 index 3669512287fa..000000000000 --- a/packages/boot/src/__tests__/integration/repository.booter.integration.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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')); - - // Remnants from Refactor -- need to add these to core - const REPOSITORIES_PREFIX = 'repositories'; - const REPOSITORIES_TAG = 'repository'; - - let app: BooterApp; - - beforeEach('reset sandbox', () => sandbox.reset()); - beforeEach(getApp); - - it('boots repositories when app.boot() is called', async () => { - const expectedBindings = [ - `${REPOSITORIES_PREFIX}.ArtifactOne`, - `${REPOSITORIES_PREFIX}.ArtifactTwo`, - ]; - - await app.boot(); - - const bindings = app.findByTag(REPOSITORIES_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/multiple.artifact.js'), - 'repositories/multiple.repository.js', - ); - - const MyApp = require(resolve(sandbox.path, 'application.js')).BooterApp; - app = new MyApp(); - } -}); diff --git a/packages/boot/src/__tests__/unit/boot.component.unit.ts b/packages/boot/src/__tests__/unit/boot.component.unit.ts index 483475d480b5..f409677cb58e 100644 --- a/packages/boot/src/__tests__/unit/boot.component.unit.ts +++ b/packages/boot/src/__tests__/unit/boot.component.unit.ts @@ -10,8 +10,6 @@ import { BootMixin, Bootstrapper, ControllerBooter, - DataSourceBooter, - RepositoryBooter, ServiceBooter, } from '../../'; @@ -34,20 +32,6 @@ describe('boot.component unit tests', () => { expect(booterInst).to.be.an.instanceOf(ControllerBooter); }); - it('RepositoryBooter is bound as a booter by default', async () => { - const booterInst = await app.get( - `${BootBindings.BOOTERS}.RepositoryBooter`, - ); - expect(booterInst).to.be.an.instanceOf(RepositoryBooter); - }); - - it('DataSourceBooter is bound as a booter by default', async () => { - const booterInst = await app.get( - `${BootBindings.BOOTERS}.DataSourceBooter`, - ); - expect(booterInst).to.be.an.instanceOf(DataSourceBooter); - }); - it('ServiceBooter is bound as a booter by default', async () => { const booterInst = await app.get(`${BootBindings.BOOTERS}.ServiceBooter`); expect(booterInst).to.be.an.instanceOf(ServiceBooter); diff --git a/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts b/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts index 9423b52b00e6..aef6d1d4b633 100644 --- a/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts +++ b/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 diff --git a/packages/boot/src/__tests__/unit/booters/datasource.booter.unit.ts b/packages/boot/src/__tests__/unit/booters/datasource.booter.unit.ts deleted file mode 100644 index b627a012bbf9..000000000000 --- a/packages/boot/src/__tests__/unit/booters/datasource.booter.unit.ts +++ /dev/null @@ -1,100 +0,0 @@ -// 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 {Application} from '@loopback/core'; -import { - ApplicationWithRepositories, - RepositoryMixin, -} from '@loopback/repository'; -import {expect, sinon, TestSandbox} from '@loopback/testlab'; -import {resolve} from 'path'; -import {DataSourceBooter, DataSourceDefaults} from '../../..'; - -describe('datasource booter unit tests', () => { - const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); - - const DATASOURCES_PREFIX = 'datasources'; - const DATASOURCES_TAG = 'datasource'; - - class AppWithRepo extends RepositoryMixin(Application) {} - - let app: AppWithRepo; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let stub: sinon.SinonStub<[any?, ...any[]], void>; - - beforeEach('reset sandbox', () => sandbox.reset()); - beforeEach(getApp); - beforeEach(createStub); - afterEach(restoreStub); - - it('gives a warning if called on an app without RepositoryMixin', async () => { - const normalApp = new Application(); - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/datasource.artifact.js'), - ); - - const booterInst = new DataSourceBooter( - normalApp as ApplicationWithRepositories, - sandbox.path, - ); - - booterInst.discovered = [resolve(sandbox.path, 'datasource.artifact.js')]; - await booterInst.load(); - - sinon.assert.calledOnce(stub); - sinon.assert.calledWith( - stub, - 'app.dataSource() function is needed for DataSourceBooter. You can add ' + - 'it to your Application using RepositoryMixin from @loopback/repository.', - ); - }); - - it(`uses DataSourceDefaults for 'options' if none are given`, () => { - const booterInst = new DataSourceBooter(app, sandbox.path); - expect(booterInst.options).to.deepEqual(DataSourceDefaults); - }); - - it('overrides defaults with provided options and uses defaults for the rest', () => { - const options = { - dirs: ['test'], - extensions: ['.ext1'], - }; - const expected = Object.assign({}, options, { - nested: DataSourceDefaults.nested, - }); - - const booterInst = new DataSourceBooter(app, sandbox.path, options); - expect(booterInst.options).to.deepEqual(expected); - }); - - it('binds datasources during the load phase', async () => { - const expected = [`${DATASOURCES_PREFIX}.db`]; - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/datasource.artifact.js'), - ); - const booterInst = new DataSourceBooter(app, sandbox.path); - const NUM_CLASSES = 1; // 1 class in above file. - - booterInst.discovered = [resolve(sandbox.path, 'datasource.artifact.js')]; - await booterInst.load(); - - const datasources = app.findByTag(DATASOURCES_TAG); - const keys = datasources.map(binding => binding.key); - expect(keys).to.have.lengthOf(NUM_CLASSES); - expect(keys.sort()).to.eql(expected.sort()); - }); - - function getApp() { - app = new AppWithRepo(); - } - - function restoreStub() { - stub.restore(); - } - - function createStub() { - stub = sinon.stub(console, 'warn'); - } -}); diff --git a/packages/boot/src/__tests__/unit/booters/repository.booter.unit.ts b/packages/boot/src/__tests__/unit/booters/repository.booter.unit.ts deleted file mode 100644 index 7ca4d349c9e5..000000000000 --- a/packages/boot/src/__tests__/unit/booters/repository.booter.unit.ts +++ /dev/null @@ -1,105 +0,0 @@ -// 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 {Application} from '@loopback/core'; -import { - ApplicationWithRepositories, - RepositoryMixin, -} from '@loopback/repository'; -import {expect, sinon, TestSandbox} from '@loopback/testlab'; -import {resolve} from 'path'; -import {RepositoryBooter, RepositoryDefaults} from '../../..'; - -describe('repository booter unit tests', () => { - const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); - - const REPOSITORIES_PREFIX = 'repositories'; - const REPOSITORIES_TAG = 'repository'; - - class RepoApp extends RepositoryMixin(Application) {} - - let app: RepoApp; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let stub: sinon.SinonStub<[any?, ...any[]], void>; - - beforeEach('reset sandbox', () => sandbox.reset()); - beforeEach(getApp); - beforeEach(createStub); - afterEach(restoreStub); - - it('gives a warning if called on an app without RepositoryMixin', async () => { - const normalApp = new Application(); - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/multiple.artifact.js'), - ); - - const booterInst = new RepositoryBooter( - normalApp as ApplicationWithRepositories, - sandbox.path, - ); - - // Load uses discovered property - booterInst.discovered = [resolve(sandbox.path, 'multiple.artifact.js')]; - await booterInst.load(); - - sinon.assert.calledOnce(stub); - sinon.assert.calledWith( - stub, - 'app.repository() function is needed for RepositoryBooter. You can add it ' + - 'to your Application using RepositoryMixin from @loopback/repository.', - ); - }); - - it(`uses RepositoryDefaults for 'options' if none are give`, () => { - const booterInst = new RepositoryBooter(app, sandbox.path); - expect(booterInst.options).to.deepEqual(RepositoryDefaults); - }); - - it('overrides defaults with provided options and uses defaults for the rest', () => { - const options = { - dirs: ['test'], - extensions: ['.ext1'], - }; - const expected = Object.assign({}, options, { - nested: RepositoryDefaults.nested, - }); - - const booterInst = new RepositoryBooter(app, sandbox.path, options); - expect(booterInst.options).to.deepEqual(expected); - }); - - it('binds repositories during the load phase', async () => { - const expected = [ - `${REPOSITORIES_PREFIX}.ArtifactOne`, - `${REPOSITORIES_PREFIX}.ArtifactTwo`, - ]; - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/multiple.artifact.js'), - ); - const booterInst = new RepositoryBooter(app, sandbox.path); - const NUM_CLASSES = 2; // 2 classes in above file. - - // Load uses discovered property - booterInst.discovered = [resolve(sandbox.path, 'multiple.artifact.js')]; - await booterInst.load(); - - const repos = app.findByTag(REPOSITORIES_TAG); - const keys = repos.map(binding => binding.key); - expect(keys).to.have.lengthOf(NUM_CLASSES); - expect(keys.sort()).to.eql(expected.sort()); - }); - - function restoreStub() { - stub.restore(); - } - - function createStub() { - stub = sinon.stub(console, 'warn'); - } - - function getApp() { - app = new RepoApp(); - } -}); diff --git a/packages/boot/src/__tests__/unit/booters/service.booter.unit.ts b/packages/boot/src/__tests__/unit/booters/service.booter.unit.ts index 60b2c77055e1..3180cb4c5da4 100644 --- a/packages/boot/src/__tests__/unit/booters/service.booter.unit.ts +++ b/packages/boot/src/__tests__/unit/booters/service.booter.unit.ts @@ -4,7 +4,6 @@ // License text available at https://opensource.org/licenses/MIT import {Application} from '@loopback/core'; -import {ApplicationWithServices, ServiceMixin} from '@loopback/service-proxy'; import {expect, sinon, TestSandbox} from '@loopback/testlab'; import {resolve} from 'path'; import {ServiceBooter, ServiceDefaults} from '../../..'; @@ -15,7 +14,7 @@ describe('service booter unit tests', () => { const SERVICES_PREFIX = 'services'; const SERVICES_TAG = 'service'; - class AppWithRepo extends ServiceMixin(Application) {} + class AppWithRepo extends Application {} let app: AppWithRepo; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -32,10 +31,7 @@ describe('service booter unit tests', () => { resolve(__dirname, '../../fixtures/service-provider.artifact.js'), ); - const booterInst = new ServiceBooter( - normalApp as ApplicationWithServices, - sandbox.path, - ); + const booterInst = new ServiceBooter(normalApp, sandbox.path); booterInst.discovered = [ resolve(sandbox.path, 'service-provider.artifact.js'), diff --git a/packages/boot/src/__tests__/unit/bootstrapper.unit.ts b/packages/boot/src/__tests__/unit/bootstrapper.unit.ts index 9ebb2cb6a2bc..292d28525983 100644 --- a/packages/boot/src/__tests__/unit/bootstrapper.unit.ts +++ b/packages/boot/src/__tests__/unit/bootstrapper.unit.ts @@ -1,17 +1,14 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 {Application} from '@loopback/core'; -import {RepositoryMixin} from '@loopback/repository'; import {expect, sinon} from '@loopback/testlab'; import {BootBindings, Booter, BootMixin, Bootstrapper} from '../..'; describe('boot-strapper unit tests', () => { - // RepositoryMixin is added to avoid warning message printed logged to console - // due to the fact that RepositoryBooter is a default Booter loaded via BootMixin. - class BootApp extends BootMixin(RepositoryMixin(Application)) {} + class BootApp extends BootMixin(Application) {} let app: BootApp; let bootstrapper: Bootstrapper; diff --git a/packages/boot/src/__tests__/unit/mixins/boot.mixin.unit.ts b/packages/boot/src/__tests__/unit/mixins/boot.mixin.unit.ts index ce065a8a4d2c..d99ba89653c9 100644 --- a/packages/boot/src/__tests__/unit/mixins/boot.mixin.unit.ts +++ b/packages/boot/src/__tests__/unit/mixins/boot.mixin.unit.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts index 0338e7b8da96..890c2f476ee4 100644 --- a/packages/boot/src/boot.component.ts +++ b/packages/boot/src/boot.component.ts @@ -5,54 +5,45 @@ import { Application, - BindingScope, + Binding, Component, + Constructor, CoreBindings, + createBindingFromClass, inject, } from '@loopback/core'; import { ApplicationMetadataBooter, ControllerBooter, - DataSourceBooter, InterceptorProviderBooter, LifeCycleObserverBooter, - ModelApiBooter, - ModelBooter, - RepositoryBooter, ServiceBooter, } from './booters'; import {Bootstrapper} from './bootstrapper'; -import {BootBindings} from './keys'; +import {Booter} from './types'; /** - * BootComponent is used to export the default list of Booter's made + * BootstrapComponent is used to export the default list of Booter's made * available by this module as well as bind the BootStrapper to the app so it * can be used to run the Booters. */ export class BootComponent implements Component { + bindings: Binding[] = [createBindingFromClass(Bootstrapper)]; // Export a list of default booters in the component so they get bound // automatically when this component is mounted. - booters = [ - ApplicationMetadataBooter, - ControllerBooter, - RepositoryBooter, - ServiceBooter, - DataSourceBooter, - LifeCycleObserverBooter, - InterceptorProviderBooter, - ModelApiBooter, - ModelBooter, - ]; + booters: Constructor[]; /** * * @param app - Application instance */ constructor(@inject(CoreBindings.APPLICATION_INSTANCE) app: Application) { - // Bound as a SINGLETON so it can be cached as it has no state - app - .bind(BootBindings.BOOTSTRAPPER_KEY) - .toClass(Bootstrapper) - .inScope(BindingScope.SINGLETON); + this.booters = [ + ApplicationMetadataBooter, + ControllerBooter, + ServiceBooter, + LifeCycleObserverBooter, + InterceptorProviderBooter, + ]; } } diff --git a/packages/boot/src/booters/booter-utils.ts b/packages/boot/src/booters/booter-utils.ts index ca232b62b7ec..6012b76fb5ef 100644 --- a/packages/boot/src/booters/booter-utils.ts +++ b/packages/boot/src/booters/booter-utils.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Copyright IBM Corp. 2018,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 diff --git a/packages/boot/src/booters/datasource.booter.ts b/packages/boot/src/booters/datasource.booter.ts deleted file mode 100644 index d8e05835bbb7..000000000000 --- a/packages/boot/src/booters/datasource.booter.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright IBM Corp. 2018,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, inject, CoreBindings} from '@loopback/core'; -import { - ApplicationWithRepositories, - Class, - juggler, -} from '@loopback/repository'; -import {BootBindings} from '../keys'; -import {ArtifactOptions, booter} from '../types'; -import {BaseArtifactBooter} from './base-artifact.booter'; - -/** - * A class that extends BaseArtifactBooter to boot the 'DataSource' artifact type. - * Discovered DataSources are bound using `app.dataSource()`. - * - * 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 - */ -@booter('datasources') -export class DataSourceBooter extends BaseArtifactBooter { - constructor( - @inject(CoreBindings.APPLICATION_INSTANCE) - public app: ApplicationWithRepositories, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, - @config() - public datasourceConfig: ArtifactOptions = {}, - ) { - super( - projectRoot, - // Set DataSource Booter Options if passed in via bootConfig - Object.assign({}, DataSourceDefaults, datasourceConfig), - ); - } - - /** - * 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(); - - /** - * If DataSource Classes were discovered, we need to make sure RepositoryMixin - * was used (so we have `app.dataSource()`) to perform the binding of a - * DataSource Class. - */ - if (this.classes.length > 0) { - if (!this.app.dataSource) { - console.warn( - 'app.dataSource() function is needed for DataSourceBooter. You can add ' + - 'it to your Application using RepositoryMixin from @loopback/repository.', - ); - } else { - this.classes.forEach(cls => { - this.app.dataSource(cls as Class); - }); - } - } - } -} - -/** - * Default ArtifactOptions for DataSourceBooter. - */ -export const DataSourceDefaults: ArtifactOptions = { - dirs: ['datasources'], - extensions: ['.datasource.js'], - nested: true, -}; diff --git a/packages/boot/src/booters/index.ts b/packages/boot/src/booters/index.ts index a3fa2e6350db..aa406dd8cf3e 100644 --- a/packages/boot/src/booters/index.ts +++ b/packages/boot/src/booters/index.ts @@ -8,10 +8,6 @@ export * from './base-artifact.booter'; export * from './booter-utils'; export * from './component-application.booter'; export * from './controller.booter'; -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-api.booter.ts b/packages/boot/src/booters/model-api.booter.ts deleted file mode 100644 index a0d71f233343..000000000000 --- a/packages/boot/src/booters/model-api.booter.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright IBM Corp. 2019. 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, - CoreBindings, - extensionPoint, - extensions, - Getter, - inject, -} from '@loopback/core'; -import { - ModelApiBuilder, - ModelApiConfig, - MODEL_API_BUILDER_PLUGINS, -} from '@loopback/model-api-builder'; -import {ApplicationWithRepositories} from '@loopback/repository'; -import debugFactory from 'debug'; -import * as path from 'path'; -import {BootBindings} from '../keys'; -import {ArtifactOptions, booter} from '../types'; -import {BaseArtifactBooter} from './base-artifact.booter'; - -const debug = debugFactory('loopback:boot:model-api'); - -@booter('modelApi') -@extensionPoint(MODEL_API_BUILDER_PLUGINS) -export class ModelApiBooter extends BaseArtifactBooter { - constructor( - @inject(CoreBindings.APPLICATION_INSTANCE) - public app: ApplicationWithRepositories, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, - @extensions() - public getModelApiBuilders: Getter, - @config() - public booterConfig: ArtifactOptions = {}, - ) { - // TODO assert that `app` has RepositoryMixin members - - super( - projectRoot, - // Set booter options if passed in via bootConfig - Object.assign({}, RestDefaults, booterConfig), - ); - } - - /** - * Load the the model config files - */ - async load(): Promise { - // Important: don't call `super.load()` here, it would try to load - // classes via `loadClassesFromFiles` - that won't work for JSON files - await Promise.all( - this.discovered.map(async f => { - try { - // It's important to await before returning, - // otherwise the catch block won't receive errors - await this.setupModel(f); - } catch (err) { - const shortPath = path.relative(this.projectRoot, f); - err.message += ` (while loading ${shortPath})`; - throw err; - } - }), - ); - } - - /** - * Set up the loaded model classes - */ - async setupModel(configFile: string): Promise { - const cfg: ModelApiConfig = require(configFile); - debug( - 'Loaded model config from %s', - path.relative(this.projectRoot, configFile), - cfg, - ); - - const modelClass = cfg.model; - if (typeof modelClass !== 'function') { - throw new Error( - `Invalid "model" field. Expected a Model class, found ${modelClass}`, - ); - } - - const builder = await this.getApiBuilderForPattern(cfg.pattern); - await builder.build(this.app, modelClass, cfg); - } - - /** - * Retrieve the API builder that matches the pattern provided - * @param pattern - name of pattern for an API builder - */ - async getApiBuilderForPattern(pattern: string): Promise { - const allBuilders = await this.getModelApiBuilders(); - const builder = allBuilders.find(b => b.pattern === pattern); - if (!builder) { - const availableBuilders = allBuilders.map(b => b.pattern).join(', '); - throw new Error( - `Unsupported API pattern "${pattern}". ` + - `Available patterns: ${availableBuilders || ''}`, - ); - } - return builder; - } -} - -/** - * Default ArtifactOptions for ControllerBooter. - */ -export const RestDefaults: ArtifactOptions = { - dirs: ['model-endpoints'], - extensions: ['-config.js'], - nested: true, -}; diff --git a/packages/boot/src/booters/model.booter.ts b/packages/boot/src/booters/model.booter.ts deleted file mode 100644 index df23cc408b35..000000000000 --- a/packages/boot/src/booters/model.booter.ts +++ /dev/null @@ -1,75 +0,0 @@ -// 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, CoreBindings} from '@loopback/core'; -import { - ApplicationWithRepositories, - 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: ApplicationWithRepositories, - @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.model(cls); - 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/repository.booter.ts b/packages/boot/src/booters/repository.booter.ts deleted file mode 100644 index 35dc5f748e20..000000000000 --- a/packages/boot/src/booters/repository.booter.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright IBM Corp. 2018,2019. 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, inject, CoreBindings} from '@loopback/core'; -import {ApplicationWithRepositories} from '@loopback/repository'; -import {BootBindings} from '../keys'; -import {ArtifactOptions, booter} from '../types'; -import {BaseArtifactBooter} from './base-artifact.booter'; - -/** - * A class that extends BaseArtifactBooter to boot the 'Repository' artifact type. - * Discovered repositories are bound using `app.repository()` which must be added - * to an Application using the `RepositoryMixin` from `@loopback/repository`. - * - * Supported phases: configure, discover, load - * - * @param app - Application instance - * @param projectRoot - Root of User Project relative to which all paths are resolved - * @param bootConfig - Repository Artifact Options Object - */ -@booter('repositories') -export class RepositoryBooter extends BaseArtifactBooter { - constructor( - @inject(CoreBindings.APPLICATION_INSTANCE) - public app: ApplicationWithRepositories, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, - @config() - public repositoryOptions: ArtifactOptions = {}, - ) { - super( - projectRoot, - // Set Repository Booter Options if passed in via bootConfig - Object.assign({}, RepositoryDefaults, repositoryOptions), - ); - } - - /** - * Uses super method to get a list of Artifact classes. Boot each class by - * binding it to the application using `app.repository(repository);` if present. - */ - async load() { - await super.load(); - /** - * If Repository Classes were discovered, we need to make sure RepositoryMixin - * was used (so we have `app.repository()`) to perform the binding of a - * Repository Class. - */ - if (this.classes.length > 0) { - if (!this.app.repository) { - console.warn( - 'app.repository() function is needed for RepositoryBooter. You can add ' + - 'it to your Application using RepositoryMixin from @loopback/repository.', - ); - } else { - this.classes.forEach(cls => { - this.app.repository(cls); - }); - } - } - } -} - -/** - * Default ArtifactOptions for RepositoryBooter. - */ -export const RepositoryDefaults: ArtifactOptions = { - dirs: ['repositories'], - extensions: ['.repository.js'], - nested: true, -}; diff --git a/packages/boot/src/booters/service.booter.ts b/packages/boot/src/booters/service.booter.ts index 737c237ebb46..fa4d374c425d 100644 --- a/packages/boot/src/booters/service.booter.ts +++ b/packages/boot/src/booters/service.booter.ts @@ -4,6 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import { + Application, BINDING_METADATA_KEY, config, Constructor, @@ -13,7 +14,6 @@ import { isDynamicValueProviderClass, MetadataInspector, } from '@loopback/core'; -import {ApplicationWithServices} from '@loopback/service-proxy'; import debugFactory from 'debug'; import {BootBindings} from '../keys'; import {ArtifactOptions, booter} from '../types'; @@ -35,7 +35,7 @@ const debug = debugFactory('loopback:boot:service-booter'); export class ServiceBooter extends BaseArtifactBooter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) - public app: ApplicationWithServices, + public app: Application, @inject(BootBindings.PROJECT_ROOT) projectRoot: string, @config() public serviceConfig: ArtifactOptions = {}, diff --git a/packages/boot/src/bootstrapper.ts b/packages/boot/src/bootstrapper.ts index 3a47d80246ca..a449b92d9445 100644 --- a/packages/boot/src/bootstrapper.ts +++ b/packages/boot/src/bootstrapper.ts @@ -1,13 +1,17 @@ -// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Copyright IBM Corp. 2018,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 { Application, + BindingScope, + config, Context, + ContextTags, CoreBindings, inject, + injectable, resolveList, } from '@loopback/core'; import debugModule from 'debug'; @@ -34,12 +38,16 @@ const debug = debugModule('loopback:boot:bootstrapper'); * @param projectRoot - The root directory of the project, relative to which all other paths are resolved * @param bootOptions - The BootOptions describing the conventions to be used by various Booters */ +@injectable({ + scope: BindingScope.SINGLETON, + tags: {[ContextTags.KEY]: BootBindings.BOOTSTRAPPER_KEY}, +}) export class Bootstrapper { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) private app: Application & Bootable, @inject(BootBindings.PROJECT_ROOT) private projectRoot: string, - @inject(BootBindings.BOOT_OPTIONS, {optional: true}) + @config() private bootOptions: BootOptions = {}, ) { // Resolve path to projectRoot and re-bind @@ -48,7 +56,7 @@ export class Bootstrapper { // This is re-bound for testing reasons where this value may be passed directly // and needs to be propagated to the Booters via DI - app.bind(BootBindings.BOOT_OPTIONS).to(this.bootOptions); + app.configure(BootBindings.BOOTSTRAPPER_KEY).to(this.bootOptions); } /** diff --git a/packages/boot/src/keys.ts b/packages/boot/src/keys.ts index 9214ffff5a19..873dfaa5d324 100644 --- a/packages/boot/src/keys.ts +++ b/packages/boot/src/keys.ts @@ -11,22 +11,23 @@ import {BootOptions} from './types'; * Namespace for boot related binding keys */ export namespace BootBindings { + /** + * Binding key for binding the BootStrapper class + */ + export const BOOTSTRAPPER_KEY = BindingKey.create( + 'application.bootstrapper', + ); /** * Binding key for boot options */ - export const BOOT_OPTIONS = BindingKey.create('boot.options'); + export const BOOT_OPTIONS = BindingKey.create( + BindingKey.buildKeyForConfig(BOOTSTRAPPER_KEY.key).toString(), + ); /** * Binding key for determining project root directory */ export const PROJECT_ROOT = BindingKey.create('boot.project_root'); - /** - * Binding key for binding the BootStrapper class - */ - export const BOOTSTRAPPER_KEY = BindingKey.create( - 'application.bootstrapper', - ); - /** * Booter binding namespace */ diff --git a/packages/boot/src/mixins/boot.mixin.ts b/packages/boot/src/mixins/boot.mixin.ts index 130c9f0e5c66..80c8aefbdba9 100644 --- a/packages/boot/src/mixins/boot.mixin.ts +++ b/packages/boot/src/mixins/boot.mixin.ts @@ -38,7 +38,7 @@ export {Binding}; * - Add a `projectRoot` property to the Class * - Adds an optional `bootOptions` property to the Class that can be used to * store the Booter conventions. - * - Adds the `BootComponent` to the Class (which binds the Bootstrapper and default Booters) + * - Adds the `BootstrapComponent` to the Class (which binds the Bootstrapper and default Booters) * - Provides the `boot()` convenience method to call Bootstrapper.boot() * - Provides the `booter()` convenience method to bind a Booter(s) to the Application * - Override `component()` to call `mountComponentBooters` diff --git a/packages/boot/tsconfig.json b/packages/boot/tsconfig.json index 15ff0870c691..4a8aa1362493 100644 --- a/packages/boot/tsconfig.json +++ b/packages/boot/tsconfig.json @@ -14,21 +14,6 @@ { "path": "../core/tsconfig.json" }, - { - "path": "../model-api-builder/tsconfig.json" - }, - { - "path": "../repository/tsconfig.json" - }, - { - "path": "../rest-crud/tsconfig.json" - }, - { - "path": "../rest/tsconfig.json" - }, - { - "path": "../service-proxy/tsconfig.json" - }, { "path": "../testlab/tsconfig.json" } From cd7abe9b7fb3c7cd8d14d6b3f8debbec6a3be935 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 31 May 2020 16:29:09 -0700 Subject: [PATCH 2/8] feat(repository): add booters for datasources, repositories, and models Signed-off-by: Raymond Feng --- packages/repository/package.json | 2 + .../__tests__/fixtures/booters/application.ts | 15 +++ .../fixtures/booters/datasource.artifact.ts | 14 +++ .../fixtures/booters/empty.artifact.ts | 6 + .../fixtures/booters/multiple-models.model.ts | 10 ++ .../fixtures/booters/multiple.artifact.ts | 20 ++++ .../fixtures/booters/no-entity.model.ts | 15 +++ .../__tests__/fixtures/booters/package.json | 19 ++++ .../fixtures/booters/product.model.ts | 15 +++ .../fixtures/booters/product.repository.ts | 17 +++ .../booters/datasource.booter.integration.ts | 42 +++++++ .../booters/model.booter.integration.ts | 55 +++++++++ .../booters/repository.booter.integration.ts | 46 ++++++++ .../unit/booters/datasource.booter.unit.ts | 101 +++++++++++++++++ .../unit/booters/repository.booter.unit.ts | 106 ++++++++++++++++++ .../src/booters/datasource.booter.ts | 77 +++++++++++++ packages/repository/src/booters/index.ts | 8 ++ .../repository/src/booters/model.booter.ts | 76 +++++++++++++ .../src/booters/repository.booter.ts | 75 +++++++++++++ packages/repository/src/index.ts | 1 + .../repository/src/mixins/repository.mixin.ts | 12 ++ packages/repository/tsconfig.json | 3 + 22 files changed, 735 insertions(+) create mode 100644 packages/repository/src/__tests__/fixtures/booters/application.ts create mode 100644 packages/repository/src/__tests__/fixtures/booters/datasource.artifact.ts create mode 100644 packages/repository/src/__tests__/fixtures/booters/empty.artifact.ts create mode 100644 packages/repository/src/__tests__/fixtures/booters/multiple-models.model.ts create mode 100644 packages/repository/src/__tests__/fixtures/booters/multiple.artifact.ts create mode 100644 packages/repository/src/__tests__/fixtures/booters/no-entity.model.ts create mode 100644 packages/repository/src/__tests__/fixtures/booters/package.json create mode 100644 packages/repository/src/__tests__/fixtures/booters/product.model.ts create mode 100644 packages/repository/src/__tests__/fixtures/booters/product.repository.ts create mode 100644 packages/repository/src/__tests__/integration/booters/datasource.booter.integration.ts create mode 100644 packages/repository/src/__tests__/integration/booters/model.booter.integration.ts create mode 100644 packages/repository/src/__tests__/integration/booters/repository.booter.integration.ts create mode 100644 packages/repository/src/__tests__/unit/booters/datasource.booter.unit.ts create mode 100644 packages/repository/src/__tests__/unit/booters/repository.booter.unit.ts create mode 100644 packages/repository/src/booters/datasource.booter.ts create mode 100644 packages/repository/src/booters/index.ts create mode 100644 packages/repository/src/booters/model.booter.ts create mode 100644 packages/repository/src/booters/repository.booter.ts diff --git a/packages/repository/package.json b/packages/repository/package.json index 4360c7c90f94..459591bf6ddc 100644 --- a/packages/repository/package.json +++ b/packages/repository/package.json @@ -22,10 +22,12 @@ "access": "public" }, "peerDependencies": { + "@loopback/boot": "^3.0.1", "@loopback/core": "^2.11.0" }, "devDependencies": { "@loopback/build": "^6.2.5", + "@loopback/boot": "^3.0.1", "@loopback/core": "^2.11.0", "@loopback/eslint-config": "^10.0.1", "@loopback/testlab": "^3.2.7", diff --git a/packages/repository/src/__tests__/fixtures/booters/application.ts b/packages/repository/src/__tests__/fixtures/booters/application.ts new file mode 100644 index 000000000000..b1ac8145e23a --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/application.ts @@ -0,0 +1,15 @@ +// Copyright IBM Corp. 2019. 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 {BootMixin} from '@loopback/boot'; +import {Application, ApplicationConfig} from '@loopback/core'; +import {RepositoryMixin} from '../../..'; + +export class BooterApp extends BootMixin(RepositoryMixin(Application)) { + constructor(options?: ApplicationConfig) { + super(options); + this.projectRoot = __dirname; + } +} diff --git a/packages/repository/src/__tests__/fixtures/booters/datasource.artifact.ts b/packages/repository/src/__tests__/fixtures/booters/datasource.artifact.ts new file mode 100644 index 000000000000..5475df73c636 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/datasource.artifact.ts @@ -0,0 +1,14 @@ +// Copyright IBM Corp. 2019. 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 {juggler} from '../../..'; + +export class DbDataSource extends juggler.DataSource { + static dataSourceName = 'db'; + + constructor() { + super({name: 'db'}); + } +} diff --git a/packages/repository/src/__tests__/fixtures/booters/empty.artifact.ts b/packages/repository/src/__tests__/fixtures/booters/empty.artifact.ts new file mode 100644 index 000000000000..28ee7fa60c2a --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/empty.artifact.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// THIS FILE IS INTENTIONALLY LEFT EMPTY! diff --git a/packages/repository/src/__tests__/fixtures/booters/multiple-models.model.ts b/packages/repository/src/__tests__/fixtures/booters/multiple-models.model.ts new file mode 100644 index 000000000000..bf9b6dafe6f6 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/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 '../../..'; + +export class Model1 extends Model {} + +export class Model2 extends Entity {} diff --git a/packages/repository/src/__tests__/fixtures/booters/multiple.artifact.ts b/packages/repository/src/__tests__/fixtures/booters/multiple.artifact.ts new file mode 100644 index 000000000000..00cc0d71e893 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/multiple.artifact.ts @@ -0,0 +1,20 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export class ArtifactOne { + one() { + return 'ControllerOne.one()'; + } +} + +export class ArtifactTwo { + two() { + return 'ControllerTwo.two()'; + } +} + +export function hello() { + return 'hello world'; +} diff --git a/packages/repository/src/__tests__/fixtures/booters/no-entity.model.ts b/packages/repository/src/__tests__/fixtures/booters/no-entity.model.ts new file mode 100644 index 000000000000..b361495dd782 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/no-entity.model.ts @@ -0,0 +1,15 @@ +// 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 {model, Model, property} from '../../..'; + +@model() +export class NoEntity extends Model { + @property({id: true}) + id: number; + + @property({required: true}) + name: string; +} diff --git a/packages/repository/src/__tests__/fixtures/booters/package.json b/packages/repository/src/__tests__/fixtures/booters/package.json new file mode 100644 index 000000000000..24d9646e6635 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/package.json @@ -0,0 +1,19 @@ +{ + "name": "boot-test-app", + "version": "1.0.0", + "description": "boot-test-app", + "keywords": [ + "loopback-application", + "loopback" + ], + "engines": { + "node": ">=10" + }, + "scripts": { + }, + "repository": { + "type": "git" + }, + "author": "IBM Corp.", + "license": "MIT" +} diff --git a/packages/repository/src/__tests__/fixtures/booters/product.model.ts b/packages/repository/src/__tests__/fixtures/booters/product.model.ts new file mode 100644 index 000000000000..582a0a777579 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/product.model.ts @@ -0,0 +1,15 @@ +// Copyright IBM Corp. 2019. 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, property} from '../../..'; + +@model() +export class Product extends Entity { + @property({id: true}) + id: number; + + @property({required: true}) + name: string; +} diff --git a/packages/repository/src/__tests__/fixtures/booters/product.repository.ts b/packages/repository/src/__tests__/fixtures/booters/product.repository.ts new file mode 100644 index 000000000000..259eafc28a86 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/booters/product.repository.ts @@ -0,0 +1,17 @@ +// 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 {inject} from '@loopback/core'; +import {DefaultCrudRepository, juggler} from '../../..'; +import {Product} from './product.model'; + +export class ProductRepository extends DefaultCrudRepository< + Product, + typeof Product.prototype.id +> { + constructor(@inject('datasources.db') dataSource: juggler.DataSource) { + super(Product, dataSource); + } +} diff --git a/packages/repository/src/__tests__/integration/booters/datasource.booter.integration.ts b/packages/repository/src/__tests__/integration/booters/datasource.booter.integration.ts new file mode 100644 index 000000000000..e353a0b2f885 --- /dev/null +++ b/packages/repository/src/__tests__/integration/booters/datasource.booter.integration.ts @@ -0,0 +1,42 @@ +// 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/booters/application'; + +describe('datasource booter integration tests', () => { + const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); + + const DATASOURCES_PREFIX = 'datasources'; + const DATASOURCES_TAG = 'datasource'; + + let app: BooterApp; + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(getApp); + + it('boots datasources when app.boot() is called', async () => { + const expectedBindings = [`${DATASOURCES_PREFIX}.db`]; + + await app.boot(); + + const bindings = app.findByTag(DATASOURCES_TAG).map(b => b.key); + expect(bindings.sort()).to.eql(expectedBindings.sort()); + }); + + async function getApp() { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/application.js'), + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/datasource.artifact.js'), + 'datasources/db.datasource.js', + ); + + const MyApp = require(resolve(sandbox.path, 'application.js')).BooterApp; + app = new MyApp(); + } +}); diff --git a/packages/repository/src/__tests__/integration/booters/model.booter.integration.ts b/packages/repository/src/__tests__/integration/booters/model.booter.integration.ts new file mode 100644 index 000000000000..dd5a88c1cf14 --- /dev/null +++ b/packages/repository/src/__tests__/integration/booters/model.booter.integration.ts @@ -0,0 +1,55 @@ +// 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 {expect, TestSandbox} from '@loopback/testlab'; +import {resolve} from 'path'; +import {BooterApp} from '../../fixtures/booters/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/booters/application.js'), + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/no-entity.model.js'), + 'models/no-entity.model.js', + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/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/repository/src/__tests__/integration/booters/repository.booter.integration.ts b/packages/repository/src/__tests__/integration/booters/repository.booter.integration.ts new file mode 100644 index 000000000000..23e5404aacc9 --- /dev/null +++ b/packages/repository/src/__tests__/integration/booters/repository.booter.integration.ts @@ -0,0 +1,46 @@ +// 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/booters/application'; + +describe('repository booter integration tests', () => { + const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); + + // Remnants from Refactor -- need to add these to core + const REPOSITORIES_PREFIX = 'repositories'; + const REPOSITORIES_TAG = 'repository'; + + let app: BooterApp; + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(getApp); + + it('boots repositories when app.boot() is called', async () => { + const expectedBindings = [ + `${REPOSITORIES_PREFIX}.ArtifactOne`, + `${REPOSITORIES_PREFIX}.ArtifactTwo`, + ]; + + await app.boot(); + + const bindings = app.findByTag(REPOSITORIES_TAG).map(b => b.key); + expect(bindings.sort()).to.eql(expectedBindings.sort()); + }); + + async function getApp() { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/application.js'), + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/multiple.artifact.js'), + 'repositories/multiple.repository.js', + ); + + const MyApp = require(resolve(sandbox.path, 'application.js')).BooterApp; + app = new MyApp(); + } +}); diff --git a/packages/repository/src/__tests__/unit/booters/datasource.booter.unit.ts b/packages/repository/src/__tests__/unit/booters/datasource.booter.unit.ts new file mode 100644 index 000000000000..8481e059f357 --- /dev/null +++ b/packages/repository/src/__tests__/unit/booters/datasource.booter.unit.ts @@ -0,0 +1,101 @@ +// 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 {Application} from '@loopback/core'; +import {expect, sinon, TestSandbox} from '@loopback/testlab'; +import {resolve} from 'path'; +import { + ApplicationWithRepositories, + DataSourceBooter, + DataSourceDefaults, + RepositoryMixin, +} from '../../..'; + +describe('datasource booter unit tests', () => { + const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); + + const DATASOURCES_PREFIX = 'datasources'; + const DATASOURCES_TAG = 'datasource'; + + class AppWithRepo extends RepositoryMixin(Application) {} + + let app: AppWithRepo; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let stub: sinon.SinonStub<[any?, ...any[]], void>; + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(getApp); + beforeEach(createStub); + afterEach(restoreStub); + + it('gives a warning if called on an app without RepositoryMixin', async () => { + const normalApp = new Application(); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/datasource.artifact.js'), + ); + + const booterInst = new DataSourceBooter( + normalApp as ApplicationWithRepositories, + sandbox.path, + ); + + booterInst.discovered = [resolve(sandbox.path, 'datasource.artifact.js')]; + await booterInst.load(); + + sinon.assert.calledOnce(stub); + sinon.assert.calledWith( + stub, + 'app.dataSource() function is needed for DataSourceBooter. You can add ' + + 'it to your Application using RepositoryMixin from @loopback/repository.', + ); + }); + + it(`uses DataSourceDefaults for 'options' if none are given`, () => { + const booterInst = new DataSourceBooter(app, sandbox.path); + expect(booterInst.options).to.deepEqual(DataSourceDefaults); + }); + + it('overrides defaults with provided options and uses defaults for the rest', () => { + const options = { + dirs: ['test'], + extensions: ['.ext1'], + }; + const expected = Object.assign({}, options, { + nested: DataSourceDefaults.nested, + }); + + const booterInst = new DataSourceBooter(app, sandbox.path, options); + expect(booterInst.options).to.deepEqual(expected); + }); + + it('binds datasources during the load phase', async () => { + const expected = [`${DATASOURCES_PREFIX}.db`]; + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/datasource.artifact.js'), + ); + const booterInst = new DataSourceBooter(app, sandbox.path); + const NUM_CLASSES = 1; // 1 class in above file. + + booterInst.discovered = [resolve(sandbox.path, 'datasource.artifact.js')]; + await booterInst.load(); + + const datasources = app.findByTag(DATASOURCES_TAG); + const keys = datasources.map(binding => binding.key); + expect(keys).to.have.lengthOf(NUM_CLASSES); + expect(keys.sort()).to.eql(expected.sort()); + }); + + function getApp() { + app = new AppWithRepo(); + } + + function restoreStub() { + stub.restore(); + } + + function createStub() { + stub = sinon.stub(console, 'warn'); + } +}); diff --git a/packages/repository/src/__tests__/unit/booters/repository.booter.unit.ts b/packages/repository/src/__tests__/unit/booters/repository.booter.unit.ts new file mode 100644 index 000000000000..1728d6a82618 --- /dev/null +++ b/packages/repository/src/__tests__/unit/booters/repository.booter.unit.ts @@ -0,0 +1,106 @@ +// 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 {Application} from '@loopback/core'; +import {expect, sinon, TestSandbox} from '@loopback/testlab'; +import {resolve} from 'path'; +import { + ApplicationWithRepositories, + RepositoryBooter, + RepositoryDefaults, + RepositoryMixin, +} from '../../..'; + +describe('repository booter unit tests', () => { + const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); + + const REPOSITORIES_PREFIX = 'repositories'; + const REPOSITORIES_TAG = 'repository'; + + class RepoApp extends RepositoryMixin(Application) {} + + let app: RepoApp; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let stub: sinon.SinonStub<[any?, ...any[]], void>; + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(getApp); + beforeEach(createStub); + afterEach(restoreStub); + + it('gives a warning if called on an app without RepositoryMixin', async () => { + const normalApp = new Application(); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/multiple.artifact.js'), + ); + + const booterInst = new RepositoryBooter( + normalApp as ApplicationWithRepositories, + sandbox.path, + ); + + // Load uses discovered property + booterInst.discovered = [resolve(sandbox.path, 'multiple.artifact.js')]; + await booterInst.load(); + + sinon.assert.calledOnce(stub); + sinon.assert.calledWith( + stub, + 'app.repository() function is needed for RepositoryBooter. You can add it ' + + 'to your Application using RepositoryMixin from @loopback/repository.', + ); + }); + + it(`uses RepositoryDefaults for 'options' if none are give`, () => { + const booterInst = new RepositoryBooter(app, sandbox.path); + expect(booterInst.options).to.deepEqual(RepositoryDefaults); + }); + + it('overrides defaults with provided options and uses defaults for the rest', () => { + const options = { + dirs: ['test'], + extensions: ['.ext1'], + }; + const expected = Object.assign({}, options, { + nested: RepositoryDefaults.nested, + }); + + const booterInst = new RepositoryBooter(app, sandbox.path, options); + expect(booterInst.options).to.deepEqual(expected); + }); + + it('binds repositories during the load phase', async () => { + const expected = [ + `${REPOSITORIES_PREFIX}.ArtifactOne`, + `${REPOSITORIES_PREFIX}.ArtifactTwo`, + ]; + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/booters/multiple.artifact.js'), + ); + const booterInst = new RepositoryBooter(app, sandbox.path); + const NUM_CLASSES = 2; // 2 classes in above file. + + // Load uses discovered property + booterInst.discovered = [resolve(sandbox.path, 'multiple.artifact.js')]; + await booterInst.load(); + + const repos = app.findByTag(REPOSITORIES_TAG); + const keys = repos.map(binding => binding.key); + expect(keys).to.have.lengthOf(NUM_CLASSES); + expect(keys.sort()).to.eql(expected.sort()); + }); + + function restoreStub() { + stub.restore(); + } + + function createStub() { + stub = sinon.stub(console, 'warn'); + } + + function getApp() { + app = new RepoApp(); + } +}); diff --git a/packages/repository/src/booters/datasource.booter.ts b/packages/repository/src/booters/datasource.booter.ts new file mode 100644 index 000000000000..2e45ade9526f --- /dev/null +++ b/packages/repository/src/booters/datasource.booter.ts @@ -0,0 +1,77 @@ +// Copyright IBM Corp. 2018,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 { + ArtifactOptions, + BaseArtifactBooter, + BootBindings, + booter, +} from '@loopback/boot'; +import {config, CoreBindings, inject} from '@loopback/core'; +import {Class} from '../common-types'; +import {ApplicationWithRepositories} from '../mixins'; +import {juggler} from '../repositories'; + +/** + * A class that extends BaseArtifactBooter to boot the 'DataSource' artifact type. + * Discovered DataSources are bound using `app.dataSource()`. + * + * 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 + */ +@booter('datasources') +export class DataSourceBooter extends BaseArtifactBooter { + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: ApplicationWithRepositories, + @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @config() + public datasourceConfig: ArtifactOptions = {}, + ) { + super( + projectRoot, + // Set DataSource Booter Options if passed in via bootConfig + Object.assign({}, DataSourceDefaults, datasourceConfig), + ); + } + + /** + * 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(); + + /** + * If DataSource Classes were discovered, we need to make sure RepositoryMixin + * was used (so we have `app.dataSource()`) to perform the binding of a + * DataSource Class. + */ + if (this.classes.length > 0) { + if (!this.app.dataSource) { + console.warn( + 'app.dataSource() function is needed for DataSourceBooter. You can add ' + + 'it to your Application using RepositoryMixin from @loopback/repository.', + ); + } else { + this.classes.forEach(cls => { + this.app.dataSource(cls as Class); + }); + } + } + } +} + +/** + * Default ArtifactOptions for DataSourceBooter. + */ +export const DataSourceDefaults: ArtifactOptions = { + dirs: ['datasources'], + extensions: ['.datasource.js'], + nested: true, +}; diff --git a/packages/repository/src/booters/index.ts b/packages/repository/src/booters/index.ts new file mode 100644 index 000000000000..3154c12caa9d --- /dev/null +++ b/packages/repository/src/booters/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2018,2020. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './datasource.booter'; +export * from './model.booter'; +export * from './repository.booter'; diff --git a/packages/repository/src/booters/model.booter.ts b/packages/repository/src/booters/model.booter.ts new file mode 100644 index 000000000000..6b16a9a4ddc7 --- /dev/null +++ b/packages/repository/src/booters/model.booter.ts @@ -0,0 +1,76 @@ +// 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 { + ArtifactOptions, + BaseArtifactBooter, + BootBindings, + booter, +} from '@loopback/boot'; +import {config, Constructor, CoreBindings, inject} from '@loopback/core'; +import debugFactory from 'debug'; +import {ModelMetadataHelper} from '../decorators'; +import {ApplicationWithRepositories} from '../mixins'; + +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: ApplicationWithRepositories, + @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.model(cls); + 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/repository/src/booters/repository.booter.ts b/packages/repository/src/booters/repository.booter.ts new file mode 100644 index 000000000000..3ae10d1ab3f9 --- /dev/null +++ b/packages/repository/src/booters/repository.booter.ts @@ -0,0 +1,75 @@ +// Copyright IBM Corp. 2018,2019. 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 { + ArtifactOptions, + BaseArtifactBooter, + BootBindings, + booter, +} from '@loopback/boot'; +import {config, CoreBindings, inject} from '@loopback/core'; +import {ApplicationWithRepositories} from '../mixins'; + +/** + * A class that extends BaseArtifactBooter to boot the 'Repository' artifact type. + * Discovered repositories are bound using `app.repository()` which must be added + * to an Application using the `RepositoryMixin` from `@loopback/repository`. + * + * Supported phases: configure, discover, load + * + * @param app - Application instance + * @param projectRoot - Root of User Project relative to which all paths are resolved + * @param bootConfig - Repository Artifact Options Object + */ +@booter('repositories') +export class RepositoryBooter extends BaseArtifactBooter { + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: ApplicationWithRepositories, + @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @config() + public repositoryOptions: ArtifactOptions = {}, + ) { + super( + projectRoot, + // Set Repository Booter Options if passed in via bootConfig + Object.assign({}, RepositoryDefaults, repositoryOptions), + ); + } + + /** + * Uses super method to get a list of Artifact classes. Boot each class by + * binding it to the application using `app.repository(repository);` if present. + */ + async load() { + await super.load(); + /** + * If Repository Classes were discovered, we need to make sure RepositoryMixin + * was used (so we have `app.repository()`) to perform the binding of a + * Repository Class. + */ + if (this.classes.length > 0) { + if (!this.app.repository) { + console.warn( + 'app.repository() function is needed for RepositoryBooter. You can add ' + + 'it to your Application using RepositoryMixin from @loopback/repository.', + ); + } else { + this.classes.forEach(cls => { + this.app.repository(cls); + }); + } + } + } +} + +/** + * Default ArtifactOptions for RepositoryBooter. + */ +export const RepositoryDefaults: ArtifactOptions = { + dirs: ['repositories'], + extensions: ['.repository.js'], + nested: true, +}; diff --git a/packages/repository/src/index.ts b/packages/repository/src/index.ts index c8ca6f4e319e..edb02f9f53ae 100644 --- a/packages/repository/src/index.ts +++ b/packages/repository/src/index.ts @@ -14,6 +14,7 @@ export * from '@loopback/filter'; export {JSONSchema7 as JsonSchema} from 'json-schema'; +export * from './booters'; export * from './common-types'; export * from './connectors'; export * from './datasource'; diff --git a/packages/repository/src/mixins/repository.mixin.ts b/packages/repository/src/mixins/repository.mixin.ts index f7764f587fe9..552a725b7ade 100644 --- a/packages/repository/src/mixins/repository.mixin.ts +++ b/packages/repository/src/mixins/repository.mixin.ts @@ -3,10 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {bindBooter} from '@loopback/boot'; import { Binding, BindingFromClassOptions, BindingScope, + Context, createBindingFromClass, } from '@loopback/core'; import { @@ -17,6 +19,7 @@ import { MixinTarget, } from '@loopback/core'; import debugFactory from 'debug'; +import {DataSourceBooter, ModelBooter, RepositoryBooter} from '../booters'; import {Class} from '../common-types'; import {SchemaMigrationOptions} from '../datasource'; import {RepositoryBindings, RepositoryTags} from '../keys'; @@ -55,6 +58,15 @@ export function RepositoryMixin>( superClass: T, ) { return class extends superClass { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(...args: any[]) { + super(...args); + const ctx = (this as unknown) as Context; + bindBooter(ctx, ModelBooter); + bindBooter(ctx, DataSourceBooter); + bindBooter(ctx, RepositoryBooter); + } + /** * Add a repository to this application. * diff --git a/packages/repository/tsconfig.json b/packages/repository/tsconfig.json index bcaafd0cf140..344086758749 100644 --- a/packages/repository/tsconfig.json +++ b/packages/repository/tsconfig.json @@ -10,6 +10,9 @@ "src" ], "references": [ + { + "path": "../boot/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, From 016e39d68bf640ac715a87de6e6dc3b9f7745ba6 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 31 May 2020 16:37:58 -0700 Subject: [PATCH 3/8] feat(model-api-builder): add model api booter Signed-off-by: Raymond Feng --- packages/model-api-builder/package.json | 2 + .../model-api-builder/src/booters/index.ts | 6 + .../src/booters/model-api.booter.ts | 117 ++++++++++++++++++ packages/model-api-builder/src/index.ts | 1 + packages/model-api-builder/tsconfig.json | 3 + 5 files changed, 129 insertions(+) create mode 100644 packages/model-api-builder/src/booters/index.ts create mode 100644 packages/model-api-builder/src/booters/model-api.booter.ts diff --git a/packages/model-api-builder/package.json b/packages/model-api-builder/package.json index 432968139e09..a95f41e3a5e0 100644 --- a/packages/model-api-builder/package.json +++ b/packages/model-api-builder/package.json @@ -21,6 +21,7 @@ "access": "public" }, "peerDependencies": { + "@loopback/boot": "^3.0.1", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0" }, @@ -29,6 +30,7 @@ }, "devDependencies": { "@loopback/build": "^6.2.5", + "@loopback/boot": "^3.0.1", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0", "@types/node": "^10.17.35" diff --git a/packages/model-api-builder/src/booters/index.ts b/packages/model-api-builder/src/booters/index.ts new file mode 100644 index 000000000000..4a2fbb5a2315 --- /dev/null +++ b/packages/model-api-builder/src/booters/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018,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 + +export * from './model-api.booter'; diff --git a/packages/model-api-builder/src/booters/model-api.booter.ts b/packages/model-api-builder/src/booters/model-api.booter.ts new file mode 100644 index 000000000000..7c589a8d9a61 --- /dev/null +++ b/packages/model-api-builder/src/booters/model-api.booter.ts @@ -0,0 +1,117 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/model-api-builder +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + ArtifactOptions, + BaseArtifactBooter, + BootBindings, + booter, +} from '@loopback/boot'; +import { + config, + CoreBindings, + extensionPoint, + extensions, + Getter, + inject, +} from '@loopback/core'; +import {ApplicationWithRepositories} from '@loopback/repository'; +import debugFactory from 'debug'; +import * as path from 'path'; +import {ModelApiBuilder, MODEL_API_BUILDER_PLUGINS} from '../model-api-builder'; +import {ModelApiConfig} from '../model-api-config'; + +const debug = debugFactory('loopback:boot:model-api'); + +@booter('modelApi') +@extensionPoint(MODEL_API_BUILDER_PLUGINS) +export class ModelApiBooter extends BaseArtifactBooter { + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + public app: ApplicationWithRepositories, + @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @extensions() + public getModelApiBuilders: Getter, + @config() + public booterConfig: ArtifactOptions = {}, + ) { + // TODO assert that `app` has RepositoryMixin members + + super( + projectRoot, + // Set booter options if passed in via bootConfig + Object.assign({}, RestDefaults, booterConfig), + ); + } + + /** + * Load the the model config files + */ + async load(): Promise { + // Important: don't call `super.load()` here, it would try to load + // classes via `loadClassesFromFiles` - that won't work for JSON files + await Promise.all( + this.discovered.map(async f => { + try { + // It's important to await before returning, + // otherwise the catch block won't receive errors + await this.setupModel(f); + } catch (err) { + const shortPath = path.relative(this.projectRoot, f); + err.message += ` (while loading ${shortPath})`; + throw err; + } + }), + ); + } + + /** + * Set up the loaded model classes + */ + async setupModel(configFile: string): Promise { + const cfg: ModelApiConfig = require(configFile); + debug( + 'Loaded model config from %s', + path.relative(this.projectRoot, configFile), + cfg, + ); + + const modelClass = cfg.model; + if (typeof modelClass !== 'function') { + throw new Error( + `Invalid "model" field. Expected a Model class, found ${modelClass}`, + ); + } + + const builder = await this.getApiBuilderForPattern(cfg.pattern); + await builder.build(this.app, modelClass, cfg); + } + + /** + * Retrieve the API builder that matches the pattern provided + * @param pattern - name of pattern for an API builder + */ + async getApiBuilderForPattern(pattern: string): Promise { + const allBuilders = await this.getModelApiBuilders(); + const builder = allBuilders.find(b => b.pattern === pattern); + if (!builder) { + const availableBuilders = allBuilders.map(b => b.pattern).join(', '); + throw new Error( + `Unsupported API pattern "${pattern}". ` + + `Available patterns: ${availableBuilders || ''}`, + ); + } + return builder; + } +} + +/** + * Default ArtifactOptions for ControllerBooter. + */ +export const RestDefaults: ArtifactOptions = { + dirs: ['model-endpoints'], + extensions: ['-config.js'], + nested: true, +}; diff --git a/packages/model-api-builder/src/index.ts b/packages/model-api-builder/src/index.ts index 2abf288b7a48..32f5385439b8 100644 --- a/packages/model-api-builder/src/index.ts +++ b/packages/model-api-builder/src/index.ts @@ -15,5 +15,6 @@ * @packageDocumentation */ +export * from './booters'; export * from './model-api-builder'; export * from './model-api-config'; diff --git a/packages/model-api-builder/tsconfig.json b/packages/model-api-builder/tsconfig.json index 49dd1973c7d8..06c4c9e11d51 100644 --- a/packages/model-api-builder/tsconfig.json +++ b/packages/model-api-builder/tsconfig.json @@ -10,6 +10,9 @@ "src" ], "references": [ + { + "path": "../boot/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, From c57388385df845589acc502463842795e8e7aca5 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 31 May 2020 16:38:39 -0700 Subject: [PATCH 4/8] feat(rest-crud): add model api booter in the CrudRestComponent Signed-off-by: Raymond Feng --- packages/model-api-builder/package.json | 4 +- packages/repository/package.json | 4 +- packages/rest-crud/package.json | 2 + .../crud-rest.api-builder.acceptance.ts | 171 +++++++++++++++++ .../booters/model-api.booter.acceptance.ts | 173 ++++++++++++++++++ .../default-model-crud-rest.acceptance.ts | 6 +- .../src/__tests__/fixtures/application.ts | 16 ++ .../__tests__/fixtures/datasource.artifact.ts | 14 ++ .../src/__tests__/fixtures/no-entity.model.ts | 15 ++ .../src/__tests__/fixtures/package.json | 19 ++ .../src/__tests__/fixtures/product.model.ts | 15 ++ .../__tests__/fixtures/product.repository.ts | 17 ++ .../fixtures/stub-model-api-builder.ts | 67 +++++++ packages/rest-crud/src/crud-rest.component.ts | 10 +- packages/rest-crud/tsconfig.json | 3 + 15 files changed, 528 insertions(+), 8 deletions(-) create mode 100644 packages/rest-crud/src/__tests__/acceptance/booters/crud-rest.api-builder.acceptance.ts create mode 100644 packages/rest-crud/src/__tests__/acceptance/booters/model-api.booter.acceptance.ts create mode 100644 packages/rest-crud/src/__tests__/fixtures/application.ts create mode 100644 packages/rest-crud/src/__tests__/fixtures/datasource.artifact.ts create mode 100644 packages/rest-crud/src/__tests__/fixtures/no-entity.model.ts create mode 100644 packages/rest-crud/src/__tests__/fixtures/package.json create mode 100644 packages/rest-crud/src/__tests__/fixtures/product.model.ts create mode 100644 packages/rest-crud/src/__tests__/fixtures/product.repository.ts create mode 100644 packages/rest-crud/src/__tests__/fixtures/stub-model-api-builder.ts diff --git a/packages/model-api-builder/package.json b/packages/model-api-builder/package.json index a95f41e3a5e0..77511ef4e886 100644 --- a/packages/model-api-builder/package.json +++ b/packages/model-api-builder/package.json @@ -21,7 +21,7 @@ "access": "public" }, "peerDependencies": { - "@loopback/boot": "^3.0.1", + "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0" }, @@ -30,7 +30,7 @@ }, "devDependencies": { "@loopback/build": "^6.2.5", - "@loopback/boot": "^3.0.1", + "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0", "@types/node": "^10.17.35" diff --git a/packages/repository/package.json b/packages/repository/package.json index 459591bf6ddc..602b791563b7 100644 --- a/packages/repository/package.json +++ b/packages/repository/package.json @@ -22,12 +22,12 @@ "access": "public" }, "peerDependencies": { - "@loopback/boot": "^3.0.1", + "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0" }, "devDependencies": { "@loopback/build": "^6.2.5", - "@loopback/boot": "^3.0.1", + "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/eslint-config": "^10.0.1", "@loopback/testlab": "^3.2.7", diff --git a/packages/rest-crud/package.json b/packages/rest-crud/package.json index 803a06f37723..258da72ad1e1 100644 --- a/packages/rest-crud/package.json +++ b/packages/rest-crud/package.json @@ -21,6 +21,7 @@ "access": "public" }, "peerDependencies": { + "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0", "@loopback/rest": "^8.0.0" @@ -32,6 +33,7 @@ }, "devDependencies": { "@loopback/build": "^6.2.5", + "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0", "@loopback/rest": "^8.0.0", diff --git a/packages/rest-crud/src/__tests__/acceptance/booters/crud-rest.api-builder.acceptance.ts b/packages/rest-crud/src/__tests__/acceptance/booters/crud-rest.api-builder.acceptance.ts new file mode 100644 index 000000000000..b367728c88d3 --- /dev/null +++ b/packages/rest-crud/src/__tests__/acceptance/booters/crud-rest.api-builder.acceptance.ts @@ -0,0 +1,171 @@ +// 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 {BootMixin} from '@loopback/boot'; +import {ApplicationConfig} from '@loopback/core'; +import {ModelApiBooter} from '@loopback/model-api-builder'; +import {juggler, RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; +import {expect, givenHttpServerConfig, TestSandbox} from '@loopback/testlab'; +import {resolve} from 'path'; +import {CrudRestComponent} from '../../..'; +import {ProductRepository} from '../../fixtures/product.repository'; + +describe('CRUD rest builder acceptance tests', () => { + let app: BooterApp; + const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(givenAppWithDataSource); + + afterEach(stopApp); + + it('binds the controller and repository to the application', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/product.model.js'), + 'models/product.model.js', + ); + + // when creating the config file in a real app, make sure to use + // module.exports = {...} + // it's not used here because this is a .js file + await sandbox.writeTextFile( + 'model-endpoints/product.rest-config.js', + ` +const {Product} = require('../models/product.model'); +module.exports = { + model: Product, + pattern: 'CrudRest', + dataSource: 'db', + basePath: '/products', +}; + `, + ); + + // Boot & start the application + await app.boot(); + await app.start(); + + expect(app.getBinding('repositories.ProductRepository').key).to.eql( + 'repositories.ProductRepository', + ); + + expect(app.getBinding('controllers.ProductController').key).to.eql( + 'controllers.ProductController', + ); + }); + + it('uses bound repository class if it exists', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.writeTextFile( + 'model-endpoints/product.rest-config.js', + ` +const {Product} = require('../models/product.model'); +module.exports = { + model: Product, + pattern: 'CrudRest', + dataSource: 'db', + basePath: '/products', +}; + `, + ); + + app.repository(ProductRepository); + + const bindingName = 'repositories.ProductRepository'; + + const binding = app.getBinding(bindingName); + expect(binding.valueConstructor).to.eql(ProductRepository); + + // Boot & start the application + await app.boot(); + await app.start(); + + // Make sure it is still equal to the defined ProductRepository after + // booting + expect(app.getBinding(bindingName).valueConstructor).to.eql( + ProductRepository, + ); + + expect(app.getBinding('controllers.ProductController').key).to.eql( + 'controllers.ProductController', + ); + }); + + it('throws if there is no base path in the config', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.writeTextFile( + 'model-endpoints/product.rest-config.js', + ` +const {Product} = require('../models/product.model'); +module.exports = { + model: Product, + pattern: 'CrudRest', + dataSource: 'db', + // basePath not specified +}; + `, + ); + + // Boot the application + await expect(app.boot()).to.be.rejectedWith( + /Missing required field "basePath" in configuration for model Product./, + ); + }); + + it('throws if a Model is used instead of an Entity', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/no-entity.model.js'), + 'models/no-entity.model.js', + ); + + await sandbox.writeTextFile( + 'model-endpoints/no-entity.rest-config.js', + ` +const {NoEntity} = require('../models/no-entity.model'); +module.exports = { + // this model extends Model, not Entity + model: NoEntity, + pattern: 'CrudRest', + dataSource: 'db', + basePath: '/no-entities', +}; + `, + ); + + // Boot the application + await expect(app.boot()).to.be.rejectedWith( + /CrudRestController requires a model that extends 'Entity'./, + ); + }); + + class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) { + constructor(options?: ApplicationConfig) { + super(options); + this.projectRoot = sandbox.path; + this.booters(ModelApiBooter); + this.component(CrudRestComponent); + } + } + + async function givenAppWithDataSource() { + app = new BooterApp({ + rest: givenHttpServerConfig(), + }); + app.dataSource(new juggler.DataSource({connector: 'memory'}), 'db'); + } + + async function stopApp() { + if (app?.state === 'started') await app?.stop(); + } +}); diff --git a/packages/rest-crud/src/__tests__/acceptance/booters/model-api.booter.acceptance.ts b/packages/rest-crud/src/__tests__/acceptance/booters/model-api.booter.acceptance.ts new file mode 100644 index 000000000000..09d2eadbeff5 --- /dev/null +++ b/packages/rest-crud/src/__tests__/acceptance/booters/model-api.booter.acceptance.ts @@ -0,0 +1,173 @@ +// 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 {BootMixin} from '@loopback/boot'; +import {ApplicationConfig} from '@loopback/core'; +import {ModelApiBooter} from '@loopback/model-api-builder'; +import {juggler, RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; +import { + expect, + givenHttpServerConfig, + TestSandbox, + toJSON, +} from '@loopback/testlab'; +import {resolve} from 'path'; +import {Product} from '../../fixtures/product.model'; +import { + buildCalls, + samePatternBuildCalls, + SamePatternModelApiBuilderComponent, + similarPatternBuildCalls, + SimilarPatternModelApiBuilderComponent, + StubModelApiBuilderComponent, +} from '../../fixtures/stub-model-api-builder'; + +describe('model API booter acceptance tests', () => { + let app: BooterApp; + const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); + + beforeEach('reset sandbox', () => sandbox.reset()); + beforeEach(givenAppWithDataSource); + + afterEach(stopApp); + + it('uses the correct model API builder', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.writeTextFile( + 'model-endpoints/product.rest-config.js', + ` +const {Product} = require('../models/product.model'); +module.exports = { + model: Product, + pattern: 'stub', + dataSource: 'db', + basePath: '/products', +}; + `, + ); + + // Boot & start the application + await app.boot(); + await app.start(); + + expect(toJSON(buildCalls)).to.deepEqual( + toJSON([ + { + application: app, + modelClass: Product, + config: { + basePath: '/products', + dataSource: 'db', + pattern: 'stub', + }, + }, + ]), + ); + }); + + it('uses the API builder registered first if there is a duplicate pattern name', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.writeTextFile( + 'model-endpoints/product.rest-config.js', + ` +const {Product} = require('../models/product.model'); +module.exports = { + model: Product, + pattern: 'same', + dataSource: 'db', + basePath: '/products', +}; + `, + ); + + // Boot & start the application + await app.boot(); + await app.start(); + + // registered first + expect(toJSON(samePatternBuildCalls)).to.eql([toJSON(app)]); + + expect(similarPatternBuildCalls).to.be.empty(); + }); + + it('throws if there are no patterns matching', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.writeTextFile( + 'model-endpoints/product.rest-config.js', + ` +const {Product} = require('../models/product.model'); +module.exports = { + model: Product, + pattern: 'doesntExist', + dataSource: 'db', + basePath: '/products', +}; + `, + ); + + await expect(app.boot()).to.be.rejectedWith( + /Unsupported API pattern "doesntExist"/, + ); + }); + + it('throws if the model class is invalid', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/product.model.js'), + 'models/product.model.js', + ); + + await sandbox.writeTextFile( + 'model-endpoints/product.rest-config.js', + ` +const Product = 'product' +module.exports = { + model: Product, + pattern: 'stub', + dataSource: 'db', + basePath: '/products', +}; + `, + ); + + await expect(app.boot()).to.be.rejectedWith( + /Invalid "model" field\. Expected a Model class, found product/, + ); + }); + + class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) { + constructor(options?: ApplicationConfig) { + super(options); + this.projectRoot = sandbox.path; + this.booters(ModelApiBooter); + this.component(StubModelApiBuilderComponent); + this.component(SamePatternModelApiBuilderComponent); + this.component(SimilarPatternModelApiBuilderComponent); + } + } + + async function givenAppWithDataSource() { + app = new BooterApp({ + rest: givenHttpServerConfig(), + }); + app.dataSource(new juggler.DataSource({connector: 'memory'}), 'db'); + } + + async function stopApp() { + if (app?.state === 'started') await app?.stop(); + } +}); diff --git a/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts b/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts index b409babbe794..f50b11b89184 100644 --- a/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts +++ b/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts @@ -56,7 +56,7 @@ describe('CrudRestController for a simple Product model', () => { Object.freeze(PATCH_DATA); before(setupTestScenario); - after(stopTheApp); + after(stopApp); beforeEach(cleanDatabase); describe('create', () => { @@ -311,8 +311,8 @@ describe('CrudRestController for a simple Product model', () => { client = createRestAppClient(app); } - async function stopTheApp() { - await app.stop(); + async function stopApp() { + if (app?.state === 'started') await app?.stop(); } async function cleanDatabase() { diff --git a/packages/rest-crud/src/__tests__/fixtures/application.ts b/packages/rest-crud/src/__tests__/fixtures/application.ts new file mode 100644 index 000000000000..04f06508a1c7 --- /dev/null +++ b/packages/rest-crud/src/__tests__/fixtures/application.ts @@ -0,0 +1,16 @@ +// Copyright IBM Corp. 2019. 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 {BootMixin} from '@loopback/boot'; +import {ApplicationConfig} from '@loopback/core'; +import {RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; + +export class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) { + constructor(options?: ApplicationConfig) { + super(options); + this.projectRoot = __dirname; + } +} diff --git a/packages/rest-crud/src/__tests__/fixtures/datasource.artifact.ts b/packages/rest-crud/src/__tests__/fixtures/datasource.artifact.ts new file mode 100644 index 000000000000..5390b4b66950 --- /dev/null +++ b/packages/rest-crud/src/__tests__/fixtures/datasource.artifact.ts @@ -0,0 +1,14 @@ +// Copyright IBM Corp. 2019. 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 {juggler} from '@loopback/repository'; + +export class DbDataSource extends juggler.DataSource { + static dataSourceName = 'db'; + + constructor() { + super({name: 'db'}); + } +} diff --git a/packages/rest-crud/src/__tests__/fixtures/no-entity.model.ts b/packages/rest-crud/src/__tests__/fixtures/no-entity.model.ts new file mode 100644 index 000000000000..05d310ebc22f --- /dev/null +++ b/packages/rest-crud/src/__tests__/fixtures/no-entity.model.ts @@ -0,0 +1,15 @@ +// 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 {model, Model, property} from '@loopback/repository'; + +@model() +export class NoEntity extends Model { + @property({id: true}) + id: number; + + @property({required: true}) + name: string; +} diff --git a/packages/rest-crud/src/__tests__/fixtures/package.json b/packages/rest-crud/src/__tests__/fixtures/package.json new file mode 100644 index 000000000000..24d9646e6635 --- /dev/null +++ b/packages/rest-crud/src/__tests__/fixtures/package.json @@ -0,0 +1,19 @@ +{ + "name": "boot-test-app", + "version": "1.0.0", + "description": "boot-test-app", + "keywords": [ + "loopback-application", + "loopback" + ], + "engines": { + "node": ">=10" + }, + "scripts": { + }, + "repository": { + "type": "git" + }, + "author": "IBM Corp.", + "license": "MIT" +} diff --git a/packages/rest-crud/src/__tests__/fixtures/product.model.ts b/packages/rest-crud/src/__tests__/fixtures/product.model.ts new file mode 100644 index 000000000000..fe11f7cd037c --- /dev/null +++ b/packages/rest-crud/src/__tests__/fixtures/product.model.ts @@ -0,0 +1,15 @@ +// Copyright IBM Corp. 2019. 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, property} from '@loopback/repository'; + +@model() +export class Product extends Entity { + @property({id: true}) + id: number; + + @property({required: true}) + name: string; +} diff --git a/packages/rest-crud/src/__tests__/fixtures/product.repository.ts b/packages/rest-crud/src/__tests__/fixtures/product.repository.ts new file mode 100644 index 000000000000..471151129079 --- /dev/null +++ b/packages/rest-crud/src/__tests__/fixtures/product.repository.ts @@ -0,0 +1,17 @@ +// 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 {inject} from '@loopback/core'; +import {DefaultCrudRepository, juggler} from '@loopback/repository'; +import {Product} from './product.model'; + +export class ProductRepository extends DefaultCrudRepository< + Product, + typeof Product.prototype.id +> { + constructor(@inject('datasources.db') dataSource: juggler.DataSource) { + super(Product, dataSource); + } +} diff --git a/packages/rest-crud/src/__tests__/fixtures/stub-model-api-builder.ts b/packages/rest-crud/src/__tests__/fixtures/stub-model-api-builder.ts new file mode 100644 index 000000000000..21fbfcc58b51 --- /dev/null +++ b/packages/rest-crud/src/__tests__/fixtures/stub-model-api-builder.ts @@ -0,0 +1,67 @@ +// Copyright IBM Corp. 2019. 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 {Component, createBindingFromClass, injectable} from '@loopback/core'; +import { + asModelApiBuilder, + ModelApiBuilder, + ModelApiConfig, +} from '@loopback/model-api-builder'; +import {Model} from '@loopback/rest'; +import {BooterApp} from './application'; + +export const buildCalls: object[] = []; + +@injectable(asModelApiBuilder) +class StubModelApiBuilder implements ModelApiBuilder { + readonly pattern: string = 'stub'; + async build( + application: BooterApp, + modelClass: typeof Model & {prototype: Model}, + config: ModelApiConfig, + ): Promise { + buildCalls.push({application, modelClass, config}); + } +} + +export class StubModelApiBuilderComponent implements Component { + bindings = [createBindingFromClass(StubModelApiBuilder)]; +} + +export const samePatternBuildCalls: object[] = []; + +@injectable(asModelApiBuilder) +class SamePatternModelApiBuilder implements ModelApiBuilder { + readonly pattern: string = 'same'; + async build( + application: BooterApp, + modelClass: typeof Model & {prototype: Model}, + config: ModelApiConfig, + ): Promise { + samePatternBuildCalls.push(application); + } +} + +export class SamePatternModelApiBuilderComponent implements Component { + bindings = [createBindingFromClass(SamePatternModelApiBuilder)]; +} + +export const similarPatternBuildCalls: object[] = []; + +@injectable(asModelApiBuilder) +class SimilarPatternModelApiBuilder implements ModelApiBuilder { + readonly pattern: string = 'same'; + async build( + application: BooterApp, + modelClass: typeof Model & {prototype: Model}, + config: ModelApiConfig, + ): Promise { + similarPatternBuildCalls.push({modelClass}); + } +} + +export class SimilarPatternModelApiBuilderComponent implements Component { + bindings = [createBindingFromClass(SimilarPatternModelApiBuilder)]; +} diff --git a/packages/rest-crud/src/crud-rest.component.ts b/packages/rest-crud/src/crud-rest.component.ts index a067b98811a1..ab4125df85ba 100644 --- a/packages/rest-crud/src/crud-rest.component.ts +++ b/packages/rest-crud/src/crud-rest.component.ts @@ -3,9 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Binding, Component, createBindingFromClass} from '@loopback/core'; +import {Booter} from '@loopback/boot'; +import { + Binding, + Component, + Constructor, + createBindingFromClass, +} from '@loopback/core'; +import {ModelApiBooter} from '@loopback/model-api-builder'; import {CrudRestApiBuilder} from './crud-rest.api-builder'; export class CrudRestComponent implements Component { bindings: Binding[] = [createBindingFromClass(CrudRestApiBuilder)]; + booters: Constructor[] = [ModelApiBooter]; } diff --git a/packages/rest-crud/tsconfig.json b/packages/rest-crud/tsconfig.json index e36098a74a4c..0f57aaeca415 100644 --- a/packages/rest-crud/tsconfig.json +++ b/packages/rest-crud/tsconfig.json @@ -10,6 +10,9 @@ "src" ], "references": [ + { + "path": "../boot/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, From 065f8ad877a5981b7b292c28c4da61e4a61fd9e4 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 13 Oct 2020 09:49:56 -0700 Subject: [PATCH 5/8] feat(booter): add @loopback/booter for common boot interfaces Signed-off-by: Raymond Feng --- CODEOWNERS | 1 + docs/site/MONOREPO.md | 1 + packages/booter/.npmrc | 2 + packages/booter/LICENSE | 25 +++ packages/booter/README.md | 54 +++++++ packages/booter/package-lock.json | 137 +++++++++++++++++ packages/booter/package.json | 54 +++++++ .../src/__tests__/fixtures/empty.artifact.ts | 6 + .../fixtures/interceptor.artifact.ts | 55 +++++++ .../__tests__/fixtures/multiple.artifact.ts | 20 +++ .../src/__tests__/fixtures/package.json | 19 +++ .../booter/src/__tests__/unit/booter.unit.ts | 21 +++ .../unit/booters/base-artifact.booter.unit.ts | 80 ++++++++++ .../unit/booters/booter-utils.unit.ts | 93 ++++++++++++ packages/booter/src/base-artifact.booter.ts | 142 ++++++++++++++++++ packages/booter/src/booter-utils.ts | 110 ++++++++++++++ packages/booter/src/booter.decorator.ts | 46 ++++++ packages/booter/src/index.ts | 10 ++ packages/booter/src/keys.ts | 39 +++++ packages/booter/src/types.ts | 60 ++++++++ packages/booter/tsconfig.json | 20 +++ tsconfig.json | 3 + 22 files changed, 998 insertions(+) create mode 100644 packages/booter/.npmrc create mode 100644 packages/booter/LICENSE create mode 100644 packages/booter/README.md create mode 100644 packages/booter/package-lock.json create mode 100644 packages/booter/package.json create mode 100644 packages/booter/src/__tests__/fixtures/empty.artifact.ts create mode 100644 packages/booter/src/__tests__/fixtures/interceptor.artifact.ts create mode 100644 packages/booter/src/__tests__/fixtures/multiple.artifact.ts create mode 100644 packages/booter/src/__tests__/fixtures/package.json create mode 100644 packages/booter/src/__tests__/unit/booter.unit.ts create mode 100644 packages/booter/src/__tests__/unit/booters/base-artifact.booter.unit.ts create mode 100644 packages/booter/src/__tests__/unit/booters/booter-utils.unit.ts create mode 100644 packages/booter/src/base-artifact.booter.ts create mode 100644 packages/booter/src/booter-utils.ts create mode 100644 packages/booter/src/booter.decorator.ts create mode 100644 packages/booter/src/index.ts create mode 100644 packages/booter/src/keys.ts create mode 100644 packages/booter/src/types.ts create mode 100644 packages/booter/tsconfig.json diff --git a/CODEOWNERS b/CODEOWNERS index 144aa7383c1a..f88bfe4eddda 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,6 +246,7 @@ # - Primary owner(s): @jannyHou # - Standby owner(s): n/a /packages/boot @jannyHou +/packages/booter @raymondfeng @jannyHou # # Migration from LB3 diff --git a/docs/site/MONOREPO.md b/docs/site/MONOREPO.md index 81dcb9946a33..6ff2c4ddb336 100644 --- a/docs/site/MONOREPO.md +++ b/docs/site/MONOREPO.md @@ -56,6 +56,7 @@ one in the monorepo: `npm run update-monorepo-file` | [packages/authentication](https://github.com/strongloop/loopback-next/tree/master/packages/authentication) | @loopback/authentication | A LoopBack component for authentication support. | | [packages/authorization](https://github.com/strongloop/loopback-next/tree/master/packages/authorization) | @loopback/authorization | A LoopBack component for authorization support. | | [packages/boot](https://github.com/strongloop/loopback-next/tree/master/packages/boot) | @loopback/boot | A collection of Booters for LoopBack 4 Applications | +| [packages/booter](https://github.com/strongloop/loopback-next/tree/master/packages/booter) | @loopback/booter | @loopback/booter | | [packages/booter-lb3app](https://github.com/strongloop/loopback-next/tree/master/packages/booter-lb3app) | @loopback/booter-lb3app | A booter component for LoopBack 3 applications to expose their REST API via LoopBack 4 | | [packages/build](https://github.com/strongloop/loopback-next/tree/master/packages/build) | @loopback/build | A set of common scripts and default configurations to build LoopBack 4 or other TypeScript modules | | [packages/cli](https://github.com/strongloop/loopback-next/tree/master/packages/cli) | @loopback/cli | Yeoman generator for LoopBack 4 | diff --git a/packages/booter/.npmrc b/packages/booter/.npmrc new file mode 100644 index 000000000000..34fbbbb3f3e4 --- /dev/null +++ b/packages/booter/.npmrc @@ -0,0 +1,2 @@ +package-lock=true +scripts-prepend-node-path=true diff --git a/packages/booter/LICENSE b/packages/booter/LICENSE new file mode 100644 index 000000000000..cb40670380e5 --- /dev/null +++ b/packages/booter/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2020. +Node module: @loopback/booter +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/booter/README.md b/packages/booter/README.md new file mode 100644 index 000000000000..0ee4cf069b7f --- /dev/null +++ b/packages/booter/README.md @@ -0,0 +1,54 @@ +# @loopback/booter + +This module defines common types/interfaces for LoopBack booters that are +plugged into `@loopback/boot` as extensions. + +A Booter is a Class that can be bound to an Application and is called to perform +a task before the Application is started. A Booter may have multiple phases to +complete its task. The task for a convention based Booter is to discover and +bind Artifacts (Controllers, Repositories, Models, etc.). + +An example task of a Booter may be to discover and bind all artifacts of a given +type. + +## Installation + +```sh +$ npm install @loopback/booter +``` + +## Basic Use + +### Implement a Booter + +```ts +@booter('my-artifacts') + class MyBooter implements Booter {} +``` + +### ArtifactOptions + +| Options | Type | Description | +| ------------ | -------------------- | ------------------------------------------------------------------------------------------------------------ | +| `dirs` | `string \| string[]` | Paths relative to projectRoot to look in for Artifact | +| `extensions` | `string \| string[]` | File extensions to match for Artifact | +| `nested` | `boolean` | Look in nested directories in `dirs` for Artifact | +| `glob` | `string` | A `glob` pattern string. This takes precedence over above 3 options (which are used to make a glob pattern). | + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/booter/package-lock.json b/packages/booter/package-lock.json new file mode 100644 index 000000000000..4b3594724c62 --- /dev/null +++ b/packages/booter/package-lock.json @@ -0,0 +1,137 @@ +{ + "name": "@loopback/booter", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", + "dev": true + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "10.17.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.39.tgz", + "integrity": "sha512-dJLCxrpQmgyxYGcl0Ae9MTsQgI22qHHcGFj/8VKu7McJA5zQpnuGjoksnxbo1JxSjW/Nahnl13W8MYZf01CZHA==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + }, + "typescript": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/packages/booter/package.json b/packages/booter/package.json new file mode 100644 index 000000000000..3dd2a24843c6 --- /dev/null +++ b/packages/booter/package.json @@ -0,0 +1,54 @@ +{ + "name": "@loopback/booter", + "version": "1.0.0", + "description": "Types/interfaces for booters", + "keywords": [ + "loopback" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "engines": { + "node": "^10.16 || 12 || 14" + }, + "scripts": { + "build": "lb-tsc", + "build:watch": "lb-tsc --watch", + "pretest": "npm run clean && npm run build", + "test": "lb-mocha \"dist/__tests__/**/*.js\"", + "clean": "lb-clean dist *.tsbuildinfo .eslintcache" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git", + "directory": "packages/booter" + }, + "author": "IBM Corp.", + "license": "MIT", + "files": [ + "README.md", + "dist", + "src", + "!*/__tests__" + ], + "peerDependencies": { + "@loopback/core": "^2.11.0" + }, + "dependencies": { + "debug": "^4.2.0", + "glob": "^7.1.6", + "tslib": "^2.0.0" + }, + "devDependencies": { + "@loopback/build": "^6.2.5", + "@loopback/core": "^2.11.0", + "@loopback/testlab": "^3.2.7", + "@types/debug": "^4.1.5", + "@types/glob": "^7.1.3", + "@types/node": "^10.17.37", + "typescript": "~4.0.3" + }, + "copyright.owner": "IBM Corp.", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/booter/src/__tests__/fixtures/empty.artifact.ts b/packages/booter/src/__tests__/fixtures/empty.artifact.ts new file mode 100644 index 000000000000..28ee7fa60c2a --- /dev/null +++ b/packages/booter/src/__tests__/fixtures/empty.artifact.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// THIS FILE IS INTENTIONALLY LEFT EMPTY! diff --git a/packages/booter/src/__tests__/fixtures/interceptor.artifact.ts b/packages/booter/src/__tests__/fixtures/interceptor.artifact.ts new file mode 100644 index 000000000000..9c01657ed514 --- /dev/null +++ b/packages/booter/src/__tests__/fixtures/interceptor.artifact.ts @@ -0,0 +1,55 @@ +// 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 { + globalInterceptor, + Interceptor, + InvocationContext, + InvocationResult, + Provider, + ValueOrPromise, +} from '@loopback/core'; + +/** + * This class will be bound to the application as a global `Interceptor` during + * `boot` + */ +@globalInterceptor('auth', {tags: {name: 'myGlobalInterceptor'}}) +export class MyGlobalInterceptor implements Provider { + /* + constructor() {} + */ + + /** + * This method is used by LoopBack context to produce an interceptor function + * for the binding. + * + * @returns An interceptor function + */ + value() { + return this.intercept.bind(this); + } + + /** + * The logic to intercept an invocation + * @param invocationCtx - Invocation context + * @param next - A function to invoke next interceptor or the target method + */ + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + // eslint-disable-next-line no-useless-catch + try { + // Add pre-invocation logic here + const result = await next(); + // Add post-invocation logic here + return result; + } catch (err) { + // Add error handling logic here + throw err; + } + } +} diff --git a/packages/booter/src/__tests__/fixtures/multiple.artifact.ts b/packages/booter/src/__tests__/fixtures/multiple.artifact.ts new file mode 100644 index 000000000000..00cc0d71e893 --- /dev/null +++ b/packages/booter/src/__tests__/fixtures/multiple.artifact.ts @@ -0,0 +1,20 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export class ArtifactOne { + one() { + return 'ControllerOne.one()'; + } +} + +export class ArtifactTwo { + two() { + return 'ControllerTwo.two()'; + } +} + +export function hello() { + return 'hello world'; +} diff --git a/packages/booter/src/__tests__/fixtures/package.json b/packages/booter/src/__tests__/fixtures/package.json new file mode 100644 index 000000000000..236921b8868b --- /dev/null +++ b/packages/booter/src/__tests__/fixtures/package.json @@ -0,0 +1,19 @@ +{ + "name": "boot-test-app", + "version": "1.0.0", + "description": "boot-test-app", + "keywords": [ + "loopback-application", + "loopback" + ], + "engines": { + "node": ">=10.16" + }, + "scripts": { + }, + "repository": { + "type": "git" + }, + "author": "IBM Corp.", + "license": "MIT" +} diff --git a/packages/booter/src/__tests__/unit/booter.unit.ts b/packages/booter/src/__tests__/unit/booter.unit.ts new file mode 100644 index 000000000000..8d65630bdf75 --- /dev/null +++ b/packages/booter/src/__tests__/unit/booter.unit.ts @@ -0,0 +1,21 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/booter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {createBindingFromClass} from '@loopback/core'; +import {expect} from '@loopback/testlab'; +import {Booter, booter} from '../..'; +import {BooterBindings, BooterTags} from '../../keys'; + +describe('@booter', () => { + it('decorates a class as booter', () => { + @booter('my-artifacts') + class MyBooter implements Booter {} + + const binding = createBindingFromClass(MyBooter); + expect(binding.tagMap).to.have.property(BooterTags.BOOTER); + expect(binding.tagMap.namespace).to.equal(BooterBindings.BOOTERS); + expect(binding.key).to.equal(`${BooterBindings.BOOTERS}.MyBooter`); + }); +}); diff --git a/packages/booter/src/__tests__/unit/booters/base-artifact.booter.unit.ts b/packages/booter/src/__tests__/unit/booters/base-artifact.booter.unit.ts new file mode 100644 index 000000000000..4c0ec108038b --- /dev/null +++ b/packages/booter/src/__tests__/unit/booters/base-artifact.booter.unit.ts @@ -0,0 +1,80 @@ +// 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} from '@loopback/testlab'; +import {resolve} from 'path'; +import {ArtifactOptions, BaseArtifactBooter} from '../../..'; + +describe('base-artifact booter unit tests', () => { + const TEST_OPTIONS = { + dirs: ['test', 'test2'], + extensions: ['.test.js', 'test2.js'], + nested: false, + }; + + describe('configure()', () => { + it(`sets 'dirs' / 'extensions' properties as an array if a string`, async () => { + const booterInst = givenBaseBooter({ + dirs: 'test', + extensions: '.test.js', + nested: true, + }); + await booterInst.configure(); + expect(booterInst.dirs).to.be.eql(['test']); + expect(booterInst.extensions).to.be.eql(['.test.js']); + }); + + it(`creates and sets 'glob' pattern`, async () => { + const booterInst = givenBaseBooter(); + const expected = '/@(test|test2)/*@(.test.js|test2.js)'; + await booterInst.configure(); + expect(booterInst.glob).to.equal(expected); + }); + + it(`creates and sets 'glob' pattern (nested)`, async () => { + const booterInst = givenBaseBooter( + Object.assign({}, TEST_OPTIONS, {nested: true}), + ); + const expected = '/@(test|test2)/**/*@(.test.js|test2.js)'; + await booterInst.configure(); + expect(booterInst.glob).to.equal(expected); + }); + + it(`sets 'glob' pattern to options.glob if present`, async () => { + const expected = '/**/*.glob'; + const booterInst = givenBaseBooter( + Object.assign({}, TEST_OPTIONS, {glob: expected}), + ); + await booterInst.configure(); + expect(booterInst.glob).to.equal(expected); + }); + }); + + describe('discover()', () => { + it(`sets 'discovered' property`, async () => { + const booterInst = givenBaseBooter(); + // Fake glob pattern so we get an empty array + booterInst.glob = '/abc.xyz'; + await booterInst.discover(); + expect(booterInst.discovered).to.eql([]); + }); + }); + + describe('load()', () => { + it(`sets 'classes' property to Classes from a file`, async () => { + const booterInst = givenBaseBooter(); + booterInst.discovered = [ + resolve(__dirname, '../../fixtures/multiple.artifact.js'), + ]; + const NUM_CLASSES = 2; // Above file has 1 class in it. + await booterInst.load(); + expect(booterInst.classes).to.have.a.lengthOf(NUM_CLASSES); + }); + }); + + function givenBaseBooter(options?: ArtifactOptions) { + return new BaseArtifactBooter(__dirname, options ?? TEST_OPTIONS); + } +}); diff --git a/packages/booter/src/__tests__/unit/booters/booter-utils.unit.ts b/packages/booter/src/__tests__/unit/booters/booter-utils.unit.ts new file mode 100644 index 000000000000..44d5ef53886d --- /dev/null +++ b/packages/booter/src/__tests__/unit/booters/booter-utils.unit.ts @@ -0,0 +1,93 @@ +// 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 {discoverFiles, isClass, loadClassesFromFiles} from '../../..'; + +describe('booter-utils unit tests', () => { + const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); + + beforeEach('reset sandbox', () => sandbox.reset()); + + describe('discoverFiles()', () => { + beforeEach(setupSandbox); + + it('discovers files matching a nested glob pattern', async () => { + const expected = [ + resolve(sandbox.path, 'empty.artifact.js'), + resolve(sandbox.path, 'nested/multiple.artifact.js'), + ]; + const glob = '/**/*.artifact.js'; + + const files = await discoverFiles(glob, sandbox.path); + expect(files.sort()).to.eql(expected.sort()); + }); + + it('discovers files matching a non-nested glob pattern', async () => { + const expected = [resolve(sandbox.path, 'empty.artifact.js')]; + const glob = '/*.artifact.js'; + + const files = await discoverFiles(glob, sandbox.path); + expect(files).to.eql(expected); + }); + + it('discovers no files for a unknown glob', async () => { + const glob = '/xyz'; + const files = await discoverFiles(glob, sandbox.path); + expect(files).to.be.eql([]); + }); + + async function setupSandbox() { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/empty.artifact.js'), + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/multiple.artifact.js'), + 'nested/multiple.artifact.js', + ); + } + }); + + describe('isClass()', () => { + it('returns true given a class', () => { + expect(isClass(class Thing {})).to.be.True(); + }); + }); + + describe('loadClassesFromFiles()', () => { + it('returns an array of classes from a file', async () => { + // Copying a test file to sandbox that contains a function and 2 classes + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/multiple.artifact.js'), + ); + const files = [resolve(sandbox.path, 'multiple.artifact.js')]; + const NUM_CLASSES = 2; // Number of classes in above file + + const classes = loadClassesFromFiles(files, sandbox.path); + expect(classes).to.have.lengthOf(NUM_CLASSES); + expect(classes[0]).to.be.a.Function(); + expect(classes[1]).to.be.a.Function(); + }); + + it('returns an empty array given an empty file', async () => { + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/empty.artifact.js'), + ); + const files = [resolve(sandbox.path, 'empty.artifact.js')]; + + const classes = loadClassesFromFiles(files, sandbox.path); + expect(classes).to.be.an.Array(); + expect(classes).to.be.empty(); + }); + + it('throws an error given a non-existent file', async () => { + const files = [resolve(sandbox.path, 'fake.artifact.js')]; + expect(() => loadClassesFromFiles(files, sandbox.path)).to.throw( + /Cannot find module/, + ); + }); + }); +}); diff --git a/packages/booter/src/base-artifact.booter.ts b/packages/booter/src/base-artifact.booter.ts new file mode 100644 index 000000000000..2d6b973fd17b --- /dev/null +++ b/packages/booter/src/base-artifact.booter.ts @@ -0,0 +1,142 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/booter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor} from '@loopback/core'; +import debugFactory from 'debug'; +import path from 'path'; +import {discoverFiles, loadClassesFromFiles} from './booter-utils'; +import {ArtifactOptions, Booter} from './types'; + +const debug = debugFactory('loopback:boot:base-artifact-booter'); + +/** + * This class serves as a base class for Booters which follow a pattern of + * configure, discover files in a folder(s) using explicit folder / extensions + * or a glob pattern and lastly identifying exported classes from such files and + * performing an action on such files such as binding them. + * + * Any Booter extending this base class is expected to + * + * 1. Set the 'options' property to a object of ArtifactOptions type. (Each extending + * class should provide defaults for the ArtifactOptions and use Object.assign to merge + * the properties with user provided Options). + * 2. Provide it's own logic for 'load' after calling 'await super.load()' to + * actually boot the Artifact classes. + * + * Currently supports the following boot phases: configure, discover, load. + * + */ +export class BaseArtifactBooter implements Booter { + /** + * Options being used by the Booter. + */ + readonly options: ArtifactOptions; + /** + * Project root relative to which all other paths are resolved + */ + readonly projectRoot: string; + /** + * Relative paths of directories to be searched + */ + dirs: string[]; + /** + * File extensions to be searched + */ + extensions: string[]; + /** + * `glob` pattern to match artifact paths + */ + glob: string; + + /** + * List of files discovered by the Booter that matched artifact requirements + */ + discovered: string[]; + /** + * List of exported classes discovered in the files + */ + classes: Constructor<{}>[]; + + constructor(projectRoot: string, options: ArtifactOptions) { + this.projectRoot = projectRoot; + this.options = options; + } + + /** + * Get the name of the artifact loaded by this booter, e.g. "Controller". + * Subclasses can override the default logic based on the class name. + */ + get artifactName(): string { + return this.constructor.name.replace(/Booter$/, ''); + } + + /** + * Configure the Booter by initializing the 'dirs', 'extensions' and 'glob' + * properties. + * + * NOTE: All properties are configured even if all aren't used. + */ + async configure() { + this.dirs = this.options.dirs + ? Array.isArray(this.options.dirs) + ? this.options.dirs + : [this.options.dirs] + : []; + + this.extensions = this.options.extensions + ? Array.isArray(this.options.extensions) + ? this.options.extensions + : [this.options.extensions] + : []; + + const joinedDirs = this.dirs.join('|'); + const joinedExts = this.extensions.join('|'); + + this.glob = this.options.glob + ? this.options.glob + : `/@(${joinedDirs})/${ + this.options.nested ? '**/*' : '*' + }@(${joinedExts})`; + } + + /** + * Discover files based on the 'glob' property relative to the 'projectRoot'. + * Discovered artifact files matching the pattern are saved to the + * 'discovered' property. + */ + async discover() { + debug( + 'Discovering %s artifacts in %j using glob %j', + this.artifactName, + this.projectRoot, + this.glob, + ); + + this.discovered = await discoverFiles(this.glob, this.projectRoot); + + if (debug.enabled) { + debug( + 'Artifact files found: %s', + JSON.stringify( + this.discovered.map(f => path.relative(this.projectRoot, f)), + null, + 2, + ), + ); + } + } + + /** + * Filters the exports of 'discovered' files to only be Classes (in case + * function / types are exported) as an artifact is a Class. The filtered + * artifact Classes are saved in the 'classes' property. + * + * NOTE: Booters extending this class should call this method (await super.load()) + * and then process the artifact classes as appropriate. + */ + async load() { + this.classes = loadClassesFromFiles(this.discovered, this.projectRoot); + } +} diff --git a/packages/booter/src/booter-utils.ts b/packages/booter/src/booter-utils.ts new file mode 100644 index 000000000000..42af67423160 --- /dev/null +++ b/packages/booter/src/booter-utils.ts @@ -0,0 +1,110 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/booter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Binding, + BindingScope, + Constructor, + Context, + createBindingFromClass, +} from '@loopback/core'; +import debugFactory from 'debug'; +import globFn from 'glob'; +import path from 'path'; +import {promisify} from 'util'; +import {BooterBindings, BooterTags} from './keys'; +import {Booter} from './types'; + +const glob = promisify(globFn); + +const debug = debugFactory('loopback:boot:booter-utils'); + +/** + * Returns all files matching the given glob pattern relative to root + * + * @param pattern - A glob pattern + * @param root - Root folder to start searching for matching files + * @returns Array of discovered files + */ +export async function discoverFiles( + pattern: string, + root: string, +): Promise { + return glob(pattern, {root: root}); +} + +/** + * Given a function, returns true if it is a class, false otherwise. + * + * @param target - The function to check if it's a class or not. + * @returns True if target is a class. False otherwise. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isClass(target: any): target is Constructor { + return ( + typeof target === 'function' && target.toString().indexOf('class') === 0 + ); +} + +/** + * Returns an Array of Classes from given files. Works by requiring the file, + * identifying the exports from the file by getting the keys of the file + * and then testing each exported member to see if it's a class or not. + * + * @param files - An array of string of absolute file paths + * @param projectRootDir - The project root directory + * @returns An array of Class constructors from a file + */ +export function loadClassesFromFiles( + files: string[], + projectRootDir: string, +): Constructor<{}>[] { + const classes: Constructor<{}>[] = []; + for (const file of files) { + debug('Loading artifact file %j', path.relative(projectRootDir, file)); + const moduleObj = require(file); + for (const k in moduleObj) { + const exported = moduleObj[k]; + if (isClass(exported)) { + debug(' add %s (class %s)', k, exported.name); + classes.push(exported); + } else { + debug(' skip non-class %s', k); + } + } + } + + return classes; +} + +/** + * Method which binds a given Booter to a given Context with the Prefix and + * Tags expected by the Bootstrapper + * + * @param ctx - The Context to bind the Booter Class + * @param booterCls - Booter class to be bound + */ +export function bindBooter( + ctx: Context, + booterCls: Constructor, +): Binding { + const binding = createBindingFromClass(booterCls, { + namespace: BooterBindings.BOOTERS, + defaultScope: BindingScope.SINGLETON, + }).tag(BooterTags.BOOTER); + ctx.add(binding); + /** + * Set up configuration binding as alias to `BootBindings.BOOT_OPTIONS` + * so that the booter can use `@config`. + */ + if (binding.tagMap.artifactNamespace) { + ctx + .configure(binding.key) + .toAlias( + `${BooterBindings.BOOT_OPTIONS.key}#${binding.tagMap.artifactNamespace}`, + ); + } + return binding; +} diff --git a/packages/booter/src/booter.decorator.ts b/packages/booter/src/booter.decorator.ts new file mode 100644 index 000000000000..f767c3c2f907 --- /dev/null +++ b/packages/booter/src/booter.decorator.ts @@ -0,0 +1,46 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/booter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingSpec, ContextTags, injectable} from '@loopback/core'; +import {BooterBindings, BooterTags} from './keys'; + +/** + * `@booter` decorator to mark a class as a `Booter` and specify the artifact + * namespace for the configuration of the booter + * + * @example + * ```ts + * @booter('controllers') + * export class ControllerBooter extends BaseArtifactBooter { + * constructor( + * @inject(CoreBindings.APPLICATION_INSTANCE) public app: Application, + * @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + * @config() + * public controllerConfig: ArtifactOptions = {}, + * ) { + * // ... + * } + * } + * ``` + * + * @param artifactNamespace - Namespace for the artifact. It will be used to + * inject configuration from boot options. For example, the Booter class + * decorated with `@booter('controllers')` can receive its configuration via + * `@config()` from the `controllers` property of boot options. + * + * @param specs - Extra specs for the binding + */ +export function booter(artifactNamespace: string, ...specs: BindingSpec[]) { + return injectable( + { + tags: { + artifactNamespace, + [BooterTags.BOOTER]: BooterTags.BOOTER, + [ContextTags.NAMESPACE]: BooterBindings.BOOTERS, + }, + }, + ...specs, + ); +} diff --git a/packages/booter/src/index.ts b/packages/booter/src/index.ts new file mode 100644 index 000000000000..a2a075c432b3 --- /dev/null +++ b/packages/booter/src/index.ts @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/booter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './base-artifact.booter'; +export * from './booter-utils'; +export * from './booter.decorator'; +export * from './keys'; +export * from './types'; diff --git a/packages/booter/src/keys.ts b/packages/booter/src/keys.ts new file mode 100644 index 000000000000..8442e0397088 --- /dev/null +++ b/packages/booter/src/keys.ts @@ -0,0 +1,39 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/booter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingKey} from '@loopback/core'; + +/** + * Namespace for boot related binding keys + */ +export namespace BooterBindings { + /** + * Binding key for binding the BootStrapper class + */ + export const BOOTSTRAPPER_KEY = BindingKey.create( + 'application.bootstrapper', + ); + /** + * Binding key for boot options + */ + export const BOOT_OPTIONS = BindingKey.create( + BindingKey.buildKeyForConfig(BOOTSTRAPPER_KEY.key).toString(), + ); + /** + * Booter binding namespace + */ + export const BOOTERS = 'booters'; + /** + * Binding key for determining project root directory + */ + export const PROJECT_ROOT = BindingKey.create('boot.project_root'); +} + +/** + * Namespace for boot related tags + */ +export namespace BooterTags { + export const BOOTER = 'booter'; +} diff --git a/packages/booter/src/types.ts b/packages/booter/src/types.ts new file mode 100644 index 000000000000..55353afadf36 --- /dev/null +++ b/packages/booter/src/types.ts @@ -0,0 +1,60 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/booter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Type definition for ArtifactOptions. These are the options supported by + * this Booter. + */ +export type ArtifactOptions = { + /** + * Array of directories to check for artifacts. + * Paths must be relative. Defaults to ['controllers'] + */ + dirs?: string | string[]; + /** + * Array of file extensions to match artifact + * files in dirs. Defaults to ['.controller.js'] + */ + extensions?: string | string[]; + /** + * A flag to control if artifact discovery should check nested + * folders or not. Default to true + */ + nested?: boolean; + /** + * A `glob` string to use when searching for files. This takes + * precedence over other options. + */ + glob?: string; +}; + +/** + * Defines the requirements to implement a Booter for LoopBack applications: + * - configure() + * - discover() + * - load() + * + * A Booter will run through the above methods in order. + */ +export interface Booter { + /** + * Configure phase of the Booter. It should set options / defaults in this phase. + */ + configure?(): Promise; + /** + * Discover phase of the Booter. It should search for artifacts in this phase. + */ + discover?(): Promise; + /** + * Load phase of the Booter. It should bind the artifacts in this phase. + */ + load?(): Promise; +} + +/** + * Export of an array of all the Booter phases supported by the interface + * above, in the order they should be run. + */ +export const BOOTER_PHASES = ['configure', 'discover', 'load']; diff --git a/packages/booter/tsconfig.json b/packages/booter/tsconfig.json new file mode 100644 index 000000000000..134a68e33164 --- /dev/null +++ b/packages/booter/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": true + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core/tsconfig.json" + }, + { + "path": "../testlab/tsconfig.json" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index f59df2688492..f0dffb5dfeef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -142,6 +142,9 @@ { "path": "packages/booter-lb3app/tsconfig.json" }, + { + "path": "packages/booter/tsconfig.json" + }, { "path": "packages/context/tsconfig.json" }, From 3d0402d2610062365abe17b6564b4d48211111a6 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 13 Oct 2020 10:42:00 -0700 Subject: [PATCH 6/8] feat(boot): use `@loopback/booter` for booter related dependencies Signed-off-by: Raymond Feng --- packages/boot/package.json | 5 +- .../boot.custom-binding.component.unit.ts | 2 +- .../unit/booters/base-artifact.booter.unit.ts | 80 ---------- .../unit/booters/booter-utils.unit.ts | 93 ------------ packages/boot/src/boot.component.ts | 2 +- .../booters/application-metadata.booter.ts | 2 +- .../boot/src/booters/base-artifact.booter.ts | 142 ------------------ packages/boot/src/booters/booter-utils.ts | 70 --------- .../booters/component-application.booter.ts | 3 +- .../boot/src/booters/controller.booter.ts | 5 +- packages/boot/src/booters/index.ts | 2 - .../boot/src/booters/interceptor.booter.ts | 3 +- .../src/booters/lifecyle-observer.booter.ts | 7 +- packages/boot/src/booters/service.booter.ts | 3 +- packages/boot/src/bootstrapper.ts | 9 +- packages/boot/src/index.ts | 1 + packages/boot/src/keys.ts | 15 +- packages/boot/src/mixins/boot.mixin.ts | 37 +---- packages/boot/src/types.ts | 105 +------------ packages/boot/tsconfig.json | 3 + packages/booter/README.md | 2 +- 21 files changed, 36 insertions(+), 555 deletions(-) delete mode 100644 packages/boot/src/__tests__/unit/booters/base-artifact.booter.unit.ts delete mode 100644 packages/boot/src/__tests__/unit/booters/booter-utils.unit.ts delete mode 100644 packages/boot/src/booters/base-artifact.booter.ts delete mode 100644 packages/boot/src/booters/booter-utils.ts diff --git a/packages/boot/package.json b/packages/boot/package.json index 2c39cd26d1df..733d52a8f148 100644 --- a/packages/boot/package.json +++ b/packages/boot/package.json @@ -27,8 +27,7 @@ "@loopback/core": "^2.11.0" }, "dependencies": { - "@types/debug": "^4.1.5", - "@types/glob": "^7.1.3", + "@loopback/booter": "^1.0.0", "debug": "^4.2.0", "glob": "^7.1.6", "tslib": "^2.0.3" @@ -38,6 +37,8 @@ "@loopback/core": "^2.11.0", "@loopback/eslint-config": "^10.0.1", "@loopback/testlab": "^3.2.7", + "@types/debug": "^4.1.5", + "@types/glob": "^7.1.3", "@types/node": "^10.17.35" }, "files": [ diff --git a/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts b/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts index aef6d1d4b633..1841ea888590 100644 --- a/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts +++ b/packages/boot/src/__tests__/unit/boot.custom-binding.component.unit.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {BaseArtifactBooter} from '@loopback/booter'; import { Application, BindingKey, @@ -13,7 +14,6 @@ import { } from '@loopback/core'; import {expect} from '@loopback/testlab'; import {BootBindings, BootMixin} from '../../'; -import {BaseArtifactBooter} from '../../booters'; import {InstanceWithBooters} from '../../types'; describe('boot.component unit tests', () => { diff --git a/packages/boot/src/__tests__/unit/booters/base-artifact.booter.unit.ts b/packages/boot/src/__tests__/unit/booters/base-artifact.booter.unit.ts deleted file mode 100644 index 4c0ec108038b..000000000000 --- a/packages/boot/src/__tests__/unit/booters/base-artifact.booter.unit.ts +++ /dev/null @@ -1,80 +0,0 @@ -// 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} from '@loopback/testlab'; -import {resolve} from 'path'; -import {ArtifactOptions, BaseArtifactBooter} from '../../..'; - -describe('base-artifact booter unit tests', () => { - const TEST_OPTIONS = { - dirs: ['test', 'test2'], - extensions: ['.test.js', 'test2.js'], - nested: false, - }; - - describe('configure()', () => { - it(`sets 'dirs' / 'extensions' properties as an array if a string`, async () => { - const booterInst = givenBaseBooter({ - dirs: 'test', - extensions: '.test.js', - nested: true, - }); - await booterInst.configure(); - expect(booterInst.dirs).to.be.eql(['test']); - expect(booterInst.extensions).to.be.eql(['.test.js']); - }); - - it(`creates and sets 'glob' pattern`, async () => { - const booterInst = givenBaseBooter(); - const expected = '/@(test|test2)/*@(.test.js|test2.js)'; - await booterInst.configure(); - expect(booterInst.glob).to.equal(expected); - }); - - it(`creates and sets 'glob' pattern (nested)`, async () => { - const booterInst = givenBaseBooter( - Object.assign({}, TEST_OPTIONS, {nested: true}), - ); - const expected = '/@(test|test2)/**/*@(.test.js|test2.js)'; - await booterInst.configure(); - expect(booterInst.glob).to.equal(expected); - }); - - it(`sets 'glob' pattern to options.glob if present`, async () => { - const expected = '/**/*.glob'; - const booterInst = givenBaseBooter( - Object.assign({}, TEST_OPTIONS, {glob: expected}), - ); - await booterInst.configure(); - expect(booterInst.glob).to.equal(expected); - }); - }); - - describe('discover()', () => { - it(`sets 'discovered' property`, async () => { - const booterInst = givenBaseBooter(); - // Fake glob pattern so we get an empty array - booterInst.glob = '/abc.xyz'; - await booterInst.discover(); - expect(booterInst.discovered).to.eql([]); - }); - }); - - describe('load()', () => { - it(`sets 'classes' property to Classes from a file`, async () => { - const booterInst = givenBaseBooter(); - booterInst.discovered = [ - resolve(__dirname, '../../fixtures/multiple.artifact.js'), - ]; - const NUM_CLASSES = 2; // Above file has 1 class in it. - await booterInst.load(); - expect(booterInst.classes).to.have.a.lengthOf(NUM_CLASSES); - }); - }); - - function givenBaseBooter(options?: ArtifactOptions) { - return new BaseArtifactBooter(__dirname, options ?? TEST_OPTIONS); - } -}); diff --git a/packages/boot/src/__tests__/unit/booters/booter-utils.unit.ts b/packages/boot/src/__tests__/unit/booters/booter-utils.unit.ts deleted file mode 100644 index 44d5ef53886d..000000000000 --- a/packages/boot/src/__tests__/unit/booters/booter-utils.unit.ts +++ /dev/null @@ -1,93 +0,0 @@ -// 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 {discoverFiles, isClass, loadClassesFromFiles} from '../../..'; - -describe('booter-utils unit tests', () => { - const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox')); - - beforeEach('reset sandbox', () => sandbox.reset()); - - describe('discoverFiles()', () => { - beforeEach(setupSandbox); - - it('discovers files matching a nested glob pattern', async () => { - const expected = [ - resolve(sandbox.path, 'empty.artifact.js'), - resolve(sandbox.path, 'nested/multiple.artifact.js'), - ]; - const glob = '/**/*.artifact.js'; - - const files = await discoverFiles(glob, sandbox.path); - expect(files.sort()).to.eql(expected.sort()); - }); - - it('discovers files matching a non-nested glob pattern', async () => { - const expected = [resolve(sandbox.path, 'empty.artifact.js')]; - const glob = '/*.artifact.js'; - - const files = await discoverFiles(glob, sandbox.path); - expect(files).to.eql(expected); - }); - - it('discovers no files for a unknown glob', async () => { - const glob = '/xyz'; - const files = await discoverFiles(glob, sandbox.path); - expect(files).to.be.eql([]); - }); - - async function setupSandbox() { - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/empty.artifact.js'), - ); - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/multiple.artifact.js'), - 'nested/multiple.artifact.js', - ); - } - }); - - describe('isClass()', () => { - it('returns true given a class', () => { - expect(isClass(class Thing {})).to.be.True(); - }); - }); - - describe('loadClassesFromFiles()', () => { - it('returns an array of classes from a file', async () => { - // Copying a test file to sandbox that contains a function and 2 classes - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/multiple.artifact.js'), - ); - const files = [resolve(sandbox.path, 'multiple.artifact.js')]; - const NUM_CLASSES = 2; // Number of classes in above file - - const classes = loadClassesFromFiles(files, sandbox.path); - expect(classes).to.have.lengthOf(NUM_CLASSES); - expect(classes[0]).to.be.a.Function(); - expect(classes[1]).to.be.a.Function(); - }); - - it('returns an empty array given an empty file', async () => { - await sandbox.copyFile( - resolve(__dirname, '../../fixtures/empty.artifact.js'), - ); - const files = [resolve(sandbox.path, 'empty.artifact.js')]; - - const classes = loadClassesFromFiles(files, sandbox.path); - expect(classes).to.be.an.Array(); - expect(classes).to.be.empty(); - }); - - it('throws an error given a non-existent file', async () => { - const files = [resolve(sandbox.path, 'fake.artifact.js')]; - expect(() => loadClassesFromFiles(files, sandbox.path)).to.throw( - /Cannot find module/, - ); - }); - }); -}); diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts index 890c2f476ee4..cdee4a259aff 100644 --- a/packages/boot/src/boot.component.ts +++ b/packages/boot/src/boot.component.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {Booter} from '@loopback/booter'; import { Application, Binding, @@ -20,7 +21,6 @@ import { ServiceBooter, } from './booters'; import {Bootstrapper} from './bootstrapper'; -import {Booter} from './types'; /** * BootstrapComponent is used to export the default list of Booter's made diff --git a/packages/boot/src/booters/application-metadata.booter.ts b/packages/boot/src/booters/application-metadata.booter.ts index 45cdfafc0988..4126501d73d0 100644 --- a/packages/boot/src/booters/application-metadata.booter.ts +++ b/packages/boot/src/booters/application-metadata.booter.ts @@ -6,8 +6,8 @@ import {inject, Application, CoreBindings} from '@loopback/core'; import debugModule from 'debug'; import {BootBindings} from '../keys'; -import {Booter} from '../types'; import path = require('path'); +import {Booter} from '@loopback/booter'; const debug = debugModule('loopback:boot:booter:application-metadata'); diff --git a/packages/boot/src/booters/base-artifact.booter.ts b/packages/boot/src/booters/base-artifact.booter.ts deleted file mode 100644 index 88e535ad43ee..000000000000 --- a/packages/boot/src/booters/base-artifact.booter.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright IBM Corp. 2018,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 {Constructor} from '@loopback/core'; -import debugFactory from 'debug'; -import path from 'path'; -import {ArtifactOptions, Booter} from '../types'; -import {discoverFiles, loadClassesFromFiles} from './booter-utils'; - -const debug = debugFactory('loopback:boot:base-artifact-booter'); - -/** - * This class serves as a base class for Booters which follow a pattern of - * configure, discover files in a folder(s) using explicit folder / extensions - * or a glob pattern and lastly identifying exported classes from such files and - * performing an action on such files such as binding them. - * - * Any Booter extending this base class is expected to - * - * 1. Set the 'options' property to a object of ArtifactOptions type. (Each extending - * class should provide defaults for the ArtifactOptions and use Object.assign to merge - * the properties with user provided Options). - * 2. Provide it's own logic for 'load' after calling 'await super.load()' to - * actually boot the Artifact classes. - * - * Currently supports the following boot phases: configure, discover, load. - * - */ -export class BaseArtifactBooter implements Booter { - /** - * Options being used by the Booter. - */ - readonly options: ArtifactOptions; - /** - * Project root relative to which all other paths are resolved - */ - readonly projectRoot: string; - /** - * Relative paths of directories to be searched - */ - dirs: string[]; - /** - * File extensions to be searched - */ - extensions: string[]; - /** - * `glob` pattern to match artifact paths - */ - glob: string; - - /** - * List of files discovered by the Booter that matched artifact requirements - */ - discovered: string[]; - /** - * List of exported classes discovered in the files - */ - classes: Constructor<{}>[]; - - constructor(projectRoot: string, options: ArtifactOptions) { - this.projectRoot = projectRoot; - this.options = options; - } - - /** - * Get the name of the artifact loaded by this booter, e.g. "Controller". - * Subclasses can override the default logic based on the class name. - */ - get artifactName(): string { - return this.constructor.name.replace(/Booter$/, ''); - } - - /** - * Configure the Booter by initializing the 'dirs', 'extensions' and 'glob' - * properties. - * - * NOTE: All properties are configured even if all aren't used. - */ - async configure() { - this.dirs = this.options.dirs - ? Array.isArray(this.options.dirs) - ? this.options.dirs - : [this.options.dirs] - : []; - - this.extensions = this.options.extensions - ? Array.isArray(this.options.extensions) - ? this.options.extensions - : [this.options.extensions] - : []; - - const joinedDirs = this.dirs.join('|'); - const joinedExts = this.extensions.join('|'); - - this.glob = this.options.glob - ? this.options.glob - : `/@(${joinedDirs})/${ - this.options.nested ? '**/*' : '*' - }@(${joinedExts})`; - } - - /** - * Discover files based on the 'glob' property relative to the 'projectRoot'. - * Discovered artifact files matching the pattern are saved to the - * 'discovered' property. - */ - async discover() { - debug( - 'Discovering %s artifacts in %j using glob %j', - this.artifactName, - this.projectRoot, - this.glob, - ); - - this.discovered = await discoverFiles(this.glob, this.projectRoot); - - if (debug.enabled) { - debug( - 'Artifact files found: %s', - JSON.stringify( - this.discovered.map(f => path.relative(this.projectRoot, f)), - null, - 2, - ), - ); - } - } - - /** - * Filters the exports of 'discovered' files to only be Classes (in case - * function / types are exported) as an artifact is a Class. The filtered - * artifact Classes are saved in the 'classes' property. - * - * NOTE: Booters extending this class should call this method (await super.load()) - * and then process the artifact classes as appropriate. - */ - async load() { - this.classes = loadClassesFromFiles(this.discovered, this.projectRoot); - } -} diff --git a/packages/boot/src/booters/booter-utils.ts b/packages/boot/src/booters/booter-utils.ts deleted file mode 100644 index 6012b76fb5ef..000000000000 --- a/packages/boot/src/booters/booter-utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright IBM Corp. 2018,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 {Constructor} from '@loopback/core'; -import debugFactory from 'debug'; -import path from 'path'; -import {promisify} from 'util'; -const glob = promisify(require('glob')); - -const debug = debugFactory('loopback:boot:booter-utils'); - -/** - * Returns all files matching the given glob pattern relative to root - * - * @param pattern - A glob pattern - * @param root - Root folder to start searching for matching files - * @returns Array of discovered files - */ -export async function discoverFiles( - pattern: string, - root: string, -): Promise { - return glob(pattern, {root: root}); -} - -/** - * Given a function, returns true if it is a class, false otherwise. - * - * @param target - The function to check if it's a class or not. - * @returns True if target is a class. False otherwise. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isClass(target: any): target is Constructor { - return ( - typeof target === 'function' && target.toString().indexOf('class') === 0 - ); -} - -/** - * Returns an Array of Classes from given files. Works by requiring the file, - * identifying the exports from the file by getting the keys of the file - * and then testing each exported member to see if it's a class or not. - * - * @param files - An array of string of absolute file paths - * @param projectRootDir - The project root directory - * @returns An array of Class constructors from a file - */ -export function loadClassesFromFiles( - files: string[], - projectRootDir: string, -): Constructor<{}>[] { - const classes: Constructor<{}>[] = []; - for (const file of files) { - debug('Loading artifact file %j', path.relative(projectRootDir, file)); - const moduleObj = require(file); - for (const k in moduleObj) { - const exported = moduleObj[k]; - if (isClass(exported)) { - debug(' add %s (class %s)', k, exported.name); - classes.push(exported); - } else { - debug(' skip non-class %s', k); - } - } - } - - return classes; -} diff --git a/packages/boot/src/booters/component-application.booter.ts b/packages/boot/src/booters/component-application.booter.ts index 372df359662c..3cbb03460318 100644 --- a/packages/boot/src/booters/component-application.booter.ts +++ b/packages/boot/src/booters/component-application.booter.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {booter, Booter} from '@loopback/booter'; import { Application, Binding, @@ -14,7 +15,7 @@ import { } from '@loopback/core'; import debugFactory from 'debug'; import {BootBindings} from '../keys'; -import {Bootable, Booter, booter} from '../types'; +import {Bootable} from '../types'; const debug = debugFactory('loopback:boot:booter:component-application'); diff --git a/packages/boot/src/booters/controller.booter.ts b/packages/boot/src/booters/controller.booter.ts index 334782ec1a10..2ae8ddfab998 100644 --- a/packages/boot/src/booters/controller.booter.ts +++ b/packages/boot/src/booters/controller.booter.ts @@ -3,10 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {config, inject, Application, CoreBindings} from '@loopback/core'; +import {ArtifactOptions, BaseArtifactBooter, booter} from '@loopback/booter'; +import {Application, config, CoreBindings, inject} from '@loopback/core'; import {BootBindings} from '../keys'; -import {ArtifactOptions, booter} from '../types'; -import {BaseArtifactBooter} from './base-artifact.booter'; /** * A class that extends BaseArtifactBooter to boot the 'Controller' artifact type. diff --git a/packages/boot/src/booters/index.ts b/packages/boot/src/booters/index.ts index aa406dd8cf3e..8a7682c375e0 100644 --- a/packages/boot/src/booters/index.ts +++ b/packages/boot/src/booters/index.ts @@ -4,8 +4,6 @@ // License text available at https://opensource.org/licenses/MIT export * from './application-metadata.booter'; -export * from './base-artifact.booter'; -export * from './booter-utils'; export * from './component-application.booter'; export * from './controller.booter'; export * from './interceptor.booter'; diff --git a/packages/boot/src/booters/interceptor.booter.ts b/packages/boot/src/booters/interceptor.booter.ts index 12960eb89ac2..fd3ba7c46457 100644 --- a/packages/boot/src/booters/interceptor.booter.ts +++ b/packages/boot/src/booters/interceptor.booter.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {ArtifactOptions, BaseArtifactBooter, booter} from '@loopback/booter'; import { Application, config, @@ -14,8 +15,6 @@ import { } from '@loopback/core'; import debugFactory from 'debug'; import {BootBindings} from '../keys'; -import {ArtifactOptions, booter} from '../types'; -import {BaseArtifactBooter} from './base-artifact.booter'; const debug = debugFactory('loopback:boot:interceptor-booter'); diff --git a/packages/boot/src/booters/lifecyle-observer.booter.ts b/packages/boot/src/booters/lifecyle-observer.booter.ts index 6cdf9b824ea7..9d2854cd1cbe 100644 --- a/packages/boot/src/booters/lifecyle-observer.booter.ts +++ b/packages/boot/src/booters/lifecyle-observer.booter.ts @@ -3,17 +3,18 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {config, Constructor, inject} from '@loopback/core'; +import {ArtifactOptions, BaseArtifactBooter, booter} from '@loopback/booter'; import { Application, + config, + Constructor, CoreBindings, + inject, isLifeCycleObserverClass, LifeCycleObserver, } from '@loopback/core'; import debugFactory from 'debug'; import {BootBindings} from '../keys'; -import {ArtifactOptions, booter} from '../types'; -import {BaseArtifactBooter} from './base-artifact.booter'; const debug = debugFactory('loopback:boot:lifecycle-observer-booter'); diff --git a/packages/boot/src/booters/service.booter.ts b/packages/boot/src/booters/service.booter.ts index fa4d374c425d..4d9da872cdd2 100644 --- a/packages/boot/src/booters/service.booter.ts +++ b/packages/boot/src/booters/service.booter.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {ArtifactOptions, BaseArtifactBooter, booter} from '@loopback/booter'; import { Application, BINDING_METADATA_KEY, @@ -16,8 +17,6 @@ import { } from '@loopback/core'; import debugFactory from 'debug'; import {BootBindings} from '../keys'; -import {ArtifactOptions, booter} from '../types'; -import {BaseArtifactBooter} from './base-artifact.booter'; const debug = debugFactory('loopback:boot:service-booter'); diff --git a/packages/boot/src/bootstrapper.ts b/packages/boot/src/bootstrapper.ts index a449b92d9445..3c2ea81f37a0 100644 --- a/packages/boot/src/bootstrapper.ts +++ b/packages/boot/src/bootstrapper.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {bindBooter, BOOTER_PHASES} from '@loopback/booter'; import { Application, BindingScope, @@ -17,13 +18,7 @@ import { import debugModule from 'debug'; import {resolve} from 'path'; import {BootBindings, BootTags} from './keys'; -import {bindBooter} from './mixins'; -import { - Bootable, - BOOTER_PHASES, - BootExecutionOptions, - BootOptions, -} from './types'; +import {Bootable, BootExecutionOptions, BootOptions} from './types'; const debug = debugModule('loopback:boot:bootstrapper'); diff --git a/packages/boot/src/index.ts b/packages/boot/src/index.ts index 81916ebdf90b..62f522c0f02c 100644 --- a/packages/boot/src/index.ts +++ b/packages/boot/src/index.ts @@ -27,6 +27,7 @@ * @packageDocumentation */ +export * from '@loopback/booter'; export * from './boot.component'; export * from './booters'; export * from './bootstrapper'; diff --git a/packages/boot/src/keys.ts b/packages/boot/src/keys.ts index 873dfaa5d324..4655e008d849 100644 --- a/packages/boot/src/keys.ts +++ b/packages/boot/src/keys.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {BooterBindings, BooterTags} from '@loopback/booter'; import {BindingKey} from '@loopback/core'; import {Bootstrapper} from './bootstrapper'; import {BootOptions} from './types'; @@ -15,31 +16,31 @@ export namespace BootBindings { * Binding key for binding the BootStrapper class */ export const BOOTSTRAPPER_KEY = BindingKey.create( - 'application.bootstrapper', + BooterBindings.BOOTSTRAPPER_KEY.toString(), ); /** * Binding key for boot options */ - export const BOOT_OPTIONS = BindingKey.create( - BindingKey.buildKeyForConfig(BOOTSTRAPPER_KEY.key).toString(), + export const BOOT_OPTIONS = BindingKey.create( + BooterBindings.BOOT_OPTIONS.toString(), ); /** * Binding key for determining project root directory */ - export const PROJECT_ROOT = BindingKey.create('boot.project_root'); + export const PROJECT_ROOT = BooterBindings.PROJECT_ROOT; /** * Booter binding namespace */ - export const BOOTERS = 'booters'; - export const BOOTER_PREFIX = 'booters'; + export const BOOTERS = BooterBindings.BOOTERS; + export const BOOTER_PREFIX = BooterBindings.BOOTERS; } /** * Namespace for boot related tags */ export namespace BootTags { - export const BOOTER = 'booter'; + export const BOOTER = BooterTags.BOOTER; /** * @deprecated Use `BootTags.BOOTER` instead. */ diff --git a/packages/boot/src/mixins/boot.mixin.ts b/packages/boot/src/mixins/boot.mixin.ts index 80c8aefbdba9..dd8bba86b217 100644 --- a/packages/boot/src/mixins/boot.mixin.ts +++ b/packages/boot/src/mixins/boot.mixin.ts @@ -7,10 +7,8 @@ import { Binding, BindingFilter, BindingFromClassOptions, - BindingScope, Constructor, Context, - createBindingFromClass, Application, Component, CoreBindings, @@ -19,8 +17,9 @@ import { 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, InstanceWithBooters} from '../types'; +import {BootBindings} from '../keys'; +import {Bootable, BootOptions, InstanceWithBooters} from '../types'; +import {Booter, bindBooter} from '@loopback/booter'; // FIXME(rfeng): Workaround for https://github.com/microsoft/rushstack/pull/1867 /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -221,35 +220,5 @@ export function BootMixin>(superClass: T) { }; } -/** - * Method which binds a given Booter to a given Context with the Prefix and - * Tags expected by the Bootstrapper - * - * @param ctx - The Context to bind the Booter Class - * @param booterCls - Booter class to be bound - */ -export function bindBooter( - ctx: Context, - booterCls: Constructor, -): Binding { - const binding = createBindingFromClass(booterCls, { - namespace: BootBindings.BOOTERS, - defaultScope: BindingScope.SINGLETON, - }).tag(BootTags.BOOTER); - ctx.add(binding); - /** - * Set up configuration binding as alias to `BootBindings.BOOT_OPTIONS` - * so that the booter can use `@config`. - */ - if (binding.tagMap.artifactNamespace) { - ctx - .configure(binding.key) - .toAlias( - `${BootBindings.BOOT_OPTIONS.key}#${binding.tagMap.artifactNamespace}`, - ); - } - return binding; -} - // eslint-disable-next-line @typescript-eslint/naming-convention export const _bindBooter = bindBooter; // For backward-compatibility diff --git a/packages/boot/src/types.ts b/packages/boot/src/types.ts index 32673c12e3bf..2515e9aa0bc4 100644 --- a/packages/boot/src/types.ts +++ b/packages/boot/src/types.ts @@ -3,70 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - Binding, - BindingSpec, - Constructor, - ContextTags, - injectable, -} from '@loopback/core'; -import {BootBindings, BootTags} from './keys'; - -/** - * Type definition for ArtifactOptions. These are the options supported by - * this Booter. - */ -export type ArtifactOptions = { - /** - * Array of directories to check for artifacts. - * Paths must be relative. Defaults to ['controllers'] - */ - dirs?: string | string[]; - /** - * Array of file extensions to match artifact - * files in dirs. Defaults to ['.controller.js'] - */ - extensions?: string | string[]; - /** - * A flag to control if artifact discovery should check nested - * folders or not. Default to true - */ - nested?: boolean; - /** - * A `glob` string to use when searching for files. This takes - * precedence over other options. - */ - glob?: string; -}; - -/** - * Defines the requirements to implement a Booter for LoopBack applications: - * - configure() - * - discover() - * - load() - * - * A Booter will run through the above methods in order. - */ -export interface Booter { - /** - * Configure phase of the Booter. It should set options / defaults in this phase. - */ - configure?(): Promise; - /** - * Discover phase of the Booter. It should search for artifacts in this phase. - */ - discover?(): Promise; - /** - * Load phase of the Booter. It should bind the artifacts in this phase. - */ - load?(): Promise; -} - -/** - * Export of an array of all the Booter phases supported by the interface - * above, in the order they should be run. - */ -export const BOOTER_PHASES = ['configure', 'discover', 'load']; +import {ArtifactOptions, Booter} from '@loopback/booter'; +import {Binding, Constructor} from '@loopback/core'; /** * Options to configure `Bootstrapper` @@ -133,45 +71,6 @@ export interface Bootable { booters(...booterClasses: Constructor[]): Binding[]; } -/** - * `@booter` decorator to mark a class as a `Booter` and specify the artifact - * namespace for the configuration of the booter - * - * @example - * ```ts - * @booter('controllers') - * export class ControllerBooter extends BaseArtifactBooter { - * constructor( - * @inject(CoreBindings.APPLICATION_INSTANCE) public app: Application, - * @inject(BootBindings.PROJECT_ROOT) projectRoot: string, - * @config() - * public controllerConfig: ArtifactOptions = {}, - * ) { - * // ... - * } - * } - * ``` - * - * @param artifactNamespace - Namespace for the artifact. It will be used to - * inject configuration from boot options. For example, the Booter class - * decorated with `@booter('controllers')` can receive its configuration via - * `@config()` from the `controllers` property of boot options. - * - * @param specs - Extra specs for the binding - */ -export function booter(artifactNamespace: string, ...specs: BindingSpec[]) { - return injectable( - { - tags: { - artifactNamespace, - [BootTags.BOOTER]: BootTags.BOOTER, - [ContextTags.NAMESPACE]: BootBindings.BOOTERS, - }, - }, - ...specs, - ); -} - /** * Interface to describe an object that may have an array of `booters`. */ diff --git a/packages/boot/tsconfig.json b/packages/boot/tsconfig.json index 4a8aa1362493..4146bd1ae9f0 100644 --- a/packages/boot/tsconfig.json +++ b/packages/boot/tsconfig.json @@ -11,6 +11,9 @@ "src/**/*.json" ], "references": [ + { + "path": "../booter/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, diff --git a/packages/booter/README.md b/packages/booter/README.md index 0ee4cf069b7f..acfebf401db7 100644 --- a/packages/booter/README.md +++ b/packages/booter/README.md @@ -23,7 +23,7 @@ $ npm install @loopback/booter ```ts @booter('my-artifacts') - class MyBooter implements Booter {} +class MyBooter implements Booter {} ``` ### ArtifactOptions From 27d299ba74e0e0e95ada871a573a853cf21daa0a Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 13 Oct 2020 11:14:31 -0700 Subject: [PATCH 7/8] feat: use @loopback/booter as the peer dependency Signed-off-by: Raymond Feng --- extensions/graphql/package.json | 3 ++- extensions/graphql/src/booters/resolver.booter.ts | 6 +++--- extensions/graphql/tsconfig.json | 3 +++ extensions/typeorm/package.json | 3 ++- extensions/typeorm/src/typeorm-connection.booter.ts | 6 +++--- extensions/typeorm/tsconfig.json | 3 +++ packages/booter-lb3app/package.json | 3 ++- packages/booter-lb3app/src/lb3app.booter.ts | 6 +++--- packages/booter-lb3app/tsconfig.json | 3 +++ packages/cli/package.json | 1 + packages/model-api-builder/package.json | 3 ++- packages/model-api-builder/src/booters/model-api.booter.ts | 6 +++--- packages/model-api-builder/tsconfig.json | 3 +++ packages/repository/package.json | 3 ++- packages/repository/src/booters/datasource.booter.ts | 6 +++--- packages/repository/src/booters/model.booter.ts | 6 +++--- packages/repository/src/booters/repository.booter.ts | 6 +++--- packages/repository/src/mixins/repository.mixin.ts | 2 +- packages/repository/tsconfig.json | 3 +++ packages/rest-crud/package.json | 3 ++- packages/rest-crud/src/crud-rest.component.ts | 2 +- packages/rest-crud/tsconfig.json | 3 +++ 22 files changed, 54 insertions(+), 29 deletions(-) diff --git a/extensions/graphql/package.json b/extensions/graphql/package.json index 69226000716a..b7cdd85a5206 100644 --- a/extensions/graphql/package.json +++ b/extensions/graphql/package.json @@ -29,10 +29,11 @@ "type-graphql": "^1.1.0" }, "peerDependencies": { - "@loopback/boot": "^3.0.2", + "@loopback/booter": "^1.0.0", "@loopback/core": "^2.11.0" }, "devDependencies": { + "@loopback/booter": "^1.0.0", "@loopback/boot": "^3.0.2", "@loopback/build": "^6.2.5", "@loopback/core": "^2.11.0", diff --git a/extensions/graphql/src/booters/resolver.booter.ts b/extensions/graphql/src/booters/resolver.booter.ts index 902a92ee0933..cda644abb423 100644 --- a/extensions/graphql/src/booters/resolver.booter.ts +++ b/extensions/graphql/src/booters/resolver.booter.ts @@ -6,9 +6,9 @@ import { ArtifactOptions, BaseArtifactBooter, - BootBindings, booter, -} from '@loopback/boot'; + BooterBindings, +} from '@loopback/booter'; import { Application, config, @@ -41,7 +41,7 @@ export class GraphQLResolverBooter extends BaseArtifactBooter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) public app: Application, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(BooterBindings.PROJECT_ROOT) projectRoot: string, @config() public interceptorConfig: ArtifactOptions = {}, ) { diff --git a/extensions/graphql/tsconfig.json b/extensions/graphql/tsconfig.json index 93415ac36cdc..b2bc9a6aa548 100644 --- a/extensions/graphql/tsconfig.json +++ b/extensions/graphql/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../../packages/boot/tsconfig.json" }, + { + "path": "../../packages/booter/tsconfig.json" + }, { "path": "../../packages/core/tsconfig.json" }, diff --git a/extensions/typeorm/package.json b/extensions/typeorm/package.json index fe50ec56c0c5..fa9576d59b5a 100644 --- a/extensions/typeorm/package.json +++ b/extensions/typeorm/package.json @@ -22,7 +22,7 @@ "access": "public" }, "peerDependencies": { - "@loopback/boot": "^3.0.2", + "@loopback/booter": "^1.0.0", "@loopback/core": "^2.11.0", "@loopback/rest": "^8.0.0" }, @@ -31,6 +31,7 @@ "typeorm": "^0.2.28" }, "devDependencies": { + "@loopback/booter": "^1.0.0", "@loopback/boot": "^3.0.2", "@loopback/build": "^6.2.5", "@loopback/core": "^2.11.0", diff --git a/extensions/typeorm/src/typeorm-connection.booter.ts b/extensions/typeorm/src/typeorm-connection.booter.ts index cb6f8d270e89..c21c4acf4fb6 100644 --- a/extensions/typeorm/src/typeorm-connection.booter.ts +++ b/extensions/typeorm/src/typeorm-connection.booter.ts @@ -6,9 +6,9 @@ import { ArtifactOptions, BaseArtifactBooter, - BootBindings, booter, -} from '@loopback/boot'; + BooterBindings, +} from '@loopback/booter'; import {config, CoreBindings, inject} from '@loopback/core'; import debugFactory from 'debug'; import {ApplicationUsingTypeOrm, ConnectionOptions} from './'; @@ -28,7 +28,7 @@ export class TypeOrmConnectionBooter extends BaseArtifactBooter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) public app: ApplicationUsingTypeOrm, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(BooterBindings.PROJECT_ROOT) projectRoot: string, @config() public entityConfig: ArtifactOptions = {}, ) { diff --git a/extensions/typeorm/tsconfig.json b/extensions/typeorm/tsconfig.json index 81add39467bb..a101f6ab5a61 100644 --- a/extensions/typeorm/tsconfig.json +++ b/extensions/typeorm/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../../packages/boot/tsconfig.json" }, + { + "path": "../../packages/booter/tsconfig.json" + }, { "path": "../../packages/core/tsconfig.json" }, diff --git a/packages/booter-lb3app/package.json b/packages/booter-lb3app/package.json index 771f50777c6d..e34f57b12382 100644 --- a/packages/booter-lb3app/package.json +++ b/packages/booter-lb3app/package.json @@ -21,7 +21,7 @@ "access": "public" }, "peerDependencies": { - "@loopback/boot": "^3.0.2", + "@loopback/booter": "^1.0.0", "@loopback/core": "^2.11.0", "@loopback/rest": "^8.0.0" }, @@ -34,6 +34,7 @@ "tslib": "^2.0.3" }, "devDependencies": { + "@loopback/booter": "^1.0.0", "@loopback/boot": "^3.0.2", "@loopback/build": "^6.2.5", "@loopback/core": "^2.11.0", diff --git a/packages/booter-lb3app/src/lb3app.booter.ts b/packages/booter-lb3app/src/lb3app.booter.ts index 71e14e7be7a6..914c9fb60185 100644 --- a/packages/booter-lb3app/src/lb3app.booter.ts +++ b/packages/booter-lb3app/src/lb3app.booter.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {BootBindings, Booter} from '@loopback/boot'; +import {Booter, BooterBindings} from '@loopback/booter'; import {CoreBindings, inject} from '@loopback/core'; import { ExpressRequestHandler, @@ -35,9 +35,9 @@ export class Lb3AppBooter implements Booter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) public app: RestApplication, - @inject(BootBindings.PROJECT_ROOT) + @inject(BooterBindings.PROJECT_ROOT) public projectRoot: string, - @inject(`${BootBindings.BOOT_OPTIONS}#lb3app`) + @inject(`${BooterBindings.BOOT_OPTIONS}#lb3app`) options: Partial = {}, ) { this.options = Object.assign({}, DefaultOptions, options); diff --git a/packages/booter-lb3app/tsconfig.json b/packages/booter-lb3app/tsconfig.json index 813f7cf5178f..0ea456a790a3 100644 --- a/packages/booter-lb3app/tsconfig.json +++ b/packages/booter-lb3app/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../boot/tsconfig.json" }, + { + "path": "../booter/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, diff --git a/packages/cli/package.json b/packages/cli/package.json index e260a6b362bd..76e0f7f5a416 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -114,6 +114,7 @@ "source-map-support": "^0.5.19", "typescript": "~4.0.3", "@loopback/authentication": "^7.0.2", + "@loopback/booter": "^1.0.0", "@loopback/boot": "^3.0.2", "@loopback/build": "^6.2.5", "@loopback/cli": "^2.16.1", diff --git a/packages/model-api-builder/package.json b/packages/model-api-builder/package.json index 77511ef4e886..a16e26351df6 100644 --- a/packages/model-api-builder/package.json +++ b/packages/model-api-builder/package.json @@ -21,7 +21,7 @@ "access": "public" }, "peerDependencies": { - "@loopback/boot": "^3.0.2", + "@loopback/booter": "^1.0.0", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0" }, @@ -30,6 +30,7 @@ }, "devDependencies": { "@loopback/build": "^6.2.5", + "@loopback/booter": "^1.0.0", "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0", diff --git a/packages/model-api-builder/src/booters/model-api.booter.ts b/packages/model-api-builder/src/booters/model-api.booter.ts index 7c589a8d9a61..ff595c9ed4f7 100644 --- a/packages/model-api-builder/src/booters/model-api.booter.ts +++ b/packages/model-api-builder/src/booters/model-api.booter.ts @@ -6,9 +6,9 @@ import { ArtifactOptions, BaseArtifactBooter, - BootBindings, booter, -} from '@loopback/boot'; + BooterBindings, +} from '@loopback/booter'; import { config, CoreBindings, @@ -31,7 +31,7 @@ export class ModelApiBooter extends BaseArtifactBooter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) public app: ApplicationWithRepositories, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(BooterBindings.PROJECT_ROOT) projectRoot: string, @extensions() public getModelApiBuilders: Getter, @config() diff --git a/packages/model-api-builder/tsconfig.json b/packages/model-api-builder/tsconfig.json index 06c4c9e11d51..21cadc03c26f 100644 --- a/packages/model-api-builder/tsconfig.json +++ b/packages/model-api-builder/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../boot/tsconfig.json" }, + { + "path": "../booter/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, diff --git a/packages/repository/package.json b/packages/repository/package.json index 602b791563b7..41ce42509fa0 100644 --- a/packages/repository/package.json +++ b/packages/repository/package.json @@ -22,11 +22,12 @@ "access": "public" }, "peerDependencies": { - "@loopback/boot": "^3.0.2", + "@loopback/booter": "^1.0.0", "@loopback/core": "^2.11.0" }, "devDependencies": { "@loopback/build": "^6.2.5", + "@loopback/booter": "^1.0.0", "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/eslint-config": "^10.0.1", diff --git a/packages/repository/src/booters/datasource.booter.ts b/packages/repository/src/booters/datasource.booter.ts index 2e45ade9526f..7b23cb8e66dc 100644 --- a/packages/repository/src/booters/datasource.booter.ts +++ b/packages/repository/src/booters/datasource.booter.ts @@ -6,9 +6,9 @@ import { ArtifactOptions, BaseArtifactBooter, - BootBindings, booter, -} from '@loopback/boot'; + BooterBindings, +} from '@loopback/booter'; import {config, CoreBindings, inject} from '@loopback/core'; import {Class} from '../common-types'; import {ApplicationWithRepositories} from '../mixins'; @@ -29,7 +29,7 @@ export class DataSourceBooter extends BaseArtifactBooter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) public app: ApplicationWithRepositories, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(BooterBindings.PROJECT_ROOT) projectRoot: string, @config() public datasourceConfig: ArtifactOptions = {}, ) { diff --git a/packages/repository/src/booters/model.booter.ts b/packages/repository/src/booters/model.booter.ts index 6b16a9a4ddc7..f3ea2592b62c 100644 --- a/packages/repository/src/booters/model.booter.ts +++ b/packages/repository/src/booters/model.booter.ts @@ -6,9 +6,9 @@ import { ArtifactOptions, BaseArtifactBooter, - BootBindings, booter, -} from '@loopback/boot'; + BooterBindings, +} from '@loopback/booter'; import {config, Constructor, CoreBindings, inject} from '@loopback/core'; import debugFactory from 'debug'; import {ModelMetadataHelper} from '../decorators'; @@ -30,7 +30,7 @@ export class ModelBooter extends BaseArtifactBooter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) public app: ApplicationWithRepositories, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(BooterBindings.PROJECT_ROOT) projectRoot: string, @config() public modelConfig: ArtifactOptions = {}, ) { diff --git a/packages/repository/src/booters/repository.booter.ts b/packages/repository/src/booters/repository.booter.ts index 3ae10d1ab3f9..0f2eec6c9a77 100644 --- a/packages/repository/src/booters/repository.booter.ts +++ b/packages/repository/src/booters/repository.booter.ts @@ -6,9 +6,9 @@ import { ArtifactOptions, BaseArtifactBooter, - BootBindings, booter, -} from '@loopback/boot'; + BooterBindings, +} from '@loopback/booter'; import {config, CoreBindings, inject} from '@loopback/core'; import {ApplicationWithRepositories} from '../mixins'; @@ -28,7 +28,7 @@ export class RepositoryBooter extends BaseArtifactBooter { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) public app: ApplicationWithRepositories, - @inject(BootBindings.PROJECT_ROOT) projectRoot: string, + @inject(BooterBindings.PROJECT_ROOT) projectRoot: string, @config() public repositoryOptions: ArtifactOptions = {}, ) { diff --git a/packages/repository/src/mixins/repository.mixin.ts b/packages/repository/src/mixins/repository.mixin.ts index 552a725b7ade..ad1fdde333ae 100644 --- a/packages/repository/src/mixins/repository.mixin.ts +++ b/packages/repository/src/mixins/repository.mixin.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {bindBooter} from '@loopback/boot'; +import {bindBooter} from '@loopback/booter'; import { Binding, BindingFromClassOptions, diff --git a/packages/repository/tsconfig.json b/packages/repository/tsconfig.json index 344086758749..ca73e8da068a 100644 --- a/packages/repository/tsconfig.json +++ b/packages/repository/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../boot/tsconfig.json" }, + { + "path": "../booter/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, diff --git a/packages/rest-crud/package.json b/packages/rest-crud/package.json index 258da72ad1e1..a8c2920cd1f8 100644 --- a/packages/rest-crud/package.json +++ b/packages/rest-crud/package.json @@ -21,7 +21,7 @@ "access": "public" }, "peerDependencies": { - "@loopback/boot": "^3.0.2", + "@loopback/booter": "^1.0.0", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0", "@loopback/rest": "^8.0.0" @@ -33,6 +33,7 @@ }, "devDependencies": { "@loopback/build": "^6.2.5", + "@loopback/booter": "^1.0.0", "@loopback/boot": "^3.0.2", "@loopback/core": "^2.11.0", "@loopback/repository": "^3.1.0", diff --git a/packages/rest-crud/src/crud-rest.component.ts b/packages/rest-crud/src/crud-rest.component.ts index ab4125df85ba..c8331b160a41 100644 --- a/packages/rest-crud/src/crud-rest.component.ts +++ b/packages/rest-crud/src/crud-rest.component.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Booter} from '@loopback/boot'; +import {Booter} from '@loopback/booter'; import { Binding, Component, diff --git a/packages/rest-crud/tsconfig.json b/packages/rest-crud/tsconfig.json index 0f57aaeca415..1011c3d05be5 100644 --- a/packages/rest-crud/tsconfig.json +++ b/packages/rest-crud/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../boot/tsconfig.json" }, + { + "path": "../booter/tsconfig.json" + }, { "path": "../core/tsconfig.json" }, From 0cf4cfff3d4d29d99c48cbdd25f75bed2bfed014 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 13 Oct 2020 12:35:16 -0700 Subject: [PATCH 8/8] feat(cli): add `@loopback/booter` to project dependencies Signed-off-by: Raymond Feng --- packages/cli/generators/project/templates/package.json.ejs | 1 + packages/cli/generators/project/templates/package.plain.json.ejs | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/cli/generators/project/templates/package.json.ejs b/packages/cli/generators/project/templates/package.json.ejs index acc1a5760570..15dae002dba0 100644 --- a/packages/cli/generators/project/templates/package.json.ejs +++ b/packages/cli/generators/project/templates/package.json.ejs @@ -127,6 +127,7 @@ "dependencies": { <% if (project.projectType === 'application') { -%> "@loopback/boot": "<%= project.dependencies['@loopback/boot'] -%>", + "@loopback/booter": "<%= project.dependencies['@loopback/booter'] -%>", "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>", <% if (project.repositories) { -%> "@loopback/repository": "<%= project.dependencies['@loopback/repository'] -%>", diff --git a/packages/cli/generators/project/templates/package.plain.json.ejs b/packages/cli/generators/project/templates/package.plain.json.ejs index 445acf2ed33f..bcff5cb02fa5 100644 --- a/packages/cli/generators/project/templates/package.plain.json.ejs +++ b/packages/cli/generators/project/templates/package.plain.json.ejs @@ -130,6 +130,7 @@ "dependencies": { <% if (project.projectType === 'application') { -%> "@loopback/boot": "<%= project.dependencies['@loopback/boot'] -%>", + "@loopback/booter": "<%= project.dependencies['@loopback/booter'] -%>", "@loopback/core": "<%= project.dependencies['@loopback/core'] -%>", "@loopback/repository": "<%= project.dependencies['@loopback/repository'] -%>", <% if (project.apiconnect) { -%>