From 84d8d85d6affa88ece528bce91e6cd8f5a5a2a97 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Tue, 6 Feb 2018 19:21:03 -0500 Subject: [PATCH 01/12] feat(boot): initial project setup --- packages/boot/.npmrc | 1 + packages/boot/LICENSE | 25 ++++++++++++++++ packages/boot/index.d.ts | 6 ++++ packages/boot/index.js | 6 ++++ packages/boot/index.ts | 6 ++++ packages/boot/package.json | 50 +++++++++++++++++++++++++++++++ packages/boot/tsconfig.build.json | 8 +++++ 7 files changed, 102 insertions(+) create mode 100644 packages/boot/.npmrc create mode 100644 packages/boot/LICENSE create mode 100644 packages/boot/index.d.ts create mode 100644 packages/boot/index.js create mode 100644 packages/boot/index.ts create mode 100644 packages/boot/package.json create mode 100644 packages/boot/tsconfig.build.json diff --git a/packages/boot/.npmrc b/packages/boot/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/boot/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/boot/LICENSE b/packages/boot/LICENSE new file mode 100644 index 000000000000..bc33602fe0a5 --- /dev/null +++ b/packages/boot/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2018. All Rights Reserved. +Node module: @loopback/boot +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/boot/index.d.ts b/packages/boot/index.d.ts new file mode 100644 index 000000000000..a298c01feb0e --- /dev/null +++ b/packages/boot/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/boot/index.js b/packages/boot/index.js new file mode 100644 index 000000000000..c43b6eb7fb20 --- /dev/null +++ b/packages/boot/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/boot/index.ts b/packages/boot/index.ts new file mode 100644 index 000000000000..e1819c75d561 --- /dev/null +++ b/packages/boot/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './src'; diff --git a/packages/boot/package.json b/packages/boot/package.json new file mode 100644 index 000000000000..2f8f5b2577f6 --- /dev/null +++ b/packages/boot/package.json @@ -0,0 +1,50 @@ +{ + "name": "@loopback/boot", + "version": "4.0.0-alpha.1", + "description": "A collection of Booters for LoopBack 4 Applications", + "engines": { + "node": ">=8" + }, + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "npm run build:dist", + "build:current": "lb-tsc", + "build:dist": "lb-tsc es2017", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean loopback-boot*.tgz dist package api-docs", + "prepublishOnly": "npm run build && npm run build:apidocs", + "pretest": "npm run build:current", + "integration": "lb-mocha \"DIST/test/integration/**/*.js\"", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/integration/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-boot*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "license": "MIT", + "dependencies": { + "@loopback/context": "^4.0.0-alpha.27", + "@loopback/core": "^4.0.0-alpha.29", + "@types/debug": "0.0.30", + "@types/glob": "^5.0.34", + "debug": "^3.1.0", + "glob": "^7.1.2" + }, + "devDependencies": { + "@loopback/build": "^4.0.0-alpha.10", + "@loopback/openapi-v2": "^4.0.0-alpha.3", + "@loopback/rest": "^4.0.0-alpha.18", + "@loopback/testlab": "^4.0.0-alpha.20" + }, + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "api-docs", + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/boot/tsconfig.build.json b/packages/boot/tsconfig.build.json new file mode 100644 index 000000000000..3ffcd508d23e --- /dev/null +++ b/packages/boot/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +} From b274b570cea3f1dd4825d1da3de027fed2998338 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Tue, 6 Feb 2018 19:27:49 -0500 Subject: [PATCH 02/12] feat(boot): bootstrapper implementation --- packages/boot/README.md | 90 ++++++++++++++ packages/boot/docs.json | 14 +++ packages/boot/src/boot.component.ts | 32 +++++ packages/boot/src/bootstrapper.ts | 111 ++++++++++++++++++ packages/boot/src/index.ts | 9 ++ packages/boot/src/keys.ts | 14 +++ .../boot/test/unit/boot.component.unit.ts | 37 ++++++ packages/boot/test/unit/bootstrapper.unit.ts | 108 +++++++++++++++++ 8 files changed, 415 insertions(+) create mode 100644 packages/boot/README.md create mode 100644 packages/boot/docs.json create mode 100644 packages/boot/src/boot.component.ts create mode 100644 packages/boot/src/bootstrapper.ts create mode 100644 packages/boot/src/index.ts create mode 100644 packages/boot/src/keys.ts create mode 100644 packages/boot/test/unit/boot.component.unit.ts create mode 100644 packages/boot/test/unit/bootstrapper.unit.ts diff --git a/packages/boot/README.md b/packages/boot/README.md new file mode 100644 index 000000000000..7419d9d10898 --- /dev/null +++ b/packages/boot/README.md @@ -0,0 +1,90 @@ +# @loopback/boot + +A collection of Booters for LoopBack Applications + +# Overview + +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. + +An example task of a Booter may be to discover and bind all artifacts of a +given type. + +A BootStrapper is needed to manage the Booters and to run them. This is packaged +in BootComponent. Add `BootComponent` to your `Application` to use the default +`BootStrapper` and `Booters`. + +## Installation + +```shell +$ npm i @loopback/boot +``` + +## Basic Use + +```ts +import {Application} from '@loopback/core'; +import {BootComponent} from '@loopback/boot'; +const app = new Application(); +app.component(BootComponent); + +await app.boot({ + projectRoot: __dirname, + booters: [RepositoryBooter], // Register Booters as part of call to app.boot() + controllers: { + dirs: ['ctrl'], + extensions: ['.ctrl.js'], + nested: true + } +}); // Booter gets run by the Application +``` + +### BootOptions +List of Options available on BootOptions Object. + +|Option|Type|Description| +|-|-|-| +|`projectRoot`|`string`|Absolute path to the root of the LoopBack 4 Project. **Required**| +|`booters`|`Constructor[]`|Array of Booters to bind before booting. *Optional*| +|`filter`|`Object`|An Object to filter Booters and phases for finer control over the boot process. *Optional*| +|`filter.booters`|`string[]`|Names of Booters that should be run (all other bound booters will be ignored).| +|`filter.phases`|`string[]`|Names of phases and order that they should be run in.| + +## Available Booters + +### ControllerBooter + +#### Description +Discovers and binds Controller Classes using `app.controller()`. + +#### Options +The Options for this can be passed via `BootOptions` when calling `app.boot(options:BootOptions)`. + +The options for this are passed in a `controllers` object on `BootOptions`. + +Available Options on the `controllers` object on `BootOptions` are as follows: + +|Options|Type|Default|Description| +|-|-|-|-| +|`dirs`|`string | string[]`|`['controllers']`|Paths relative to projectRoot to look in for Controller artifacts| +|`extensions`|`string | string[]`|`['.controller.js']`|File extensions to match for Controller artifacts| +|`nested`|`boolean`|`true`|Look in nested directories in `dirs` for Controller artifacts| +|`glob`|`string`||A `glob` pattern string. This takes precendence over above 3 options (which are used to make a glob pattern).| + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing#guidelines) +- [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/boot/docs.json b/packages/boot/docs.json new file mode 100644 index 000000000000..267ffbd922ff --- /dev/null +++ b/packages/boot/docs.json @@ -0,0 +1,14 @@ +{ + "content": [ + "index.ts", + "src/booters/base-artifact.booter.ts", + "src/booters/booter-utils.ts", + "src/booters/controller.booter.ts", + "src/booters/index.ts", + "src/boot.component.ts", + "src/bootstrapper.ts", + "src/index.ts", + "src/keys.ts" + ], + "codeSectionDepth": 4 +} diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts new file mode 100644 index 000000000000..89d9ba47a404 --- /dev/null +++ b/packages/boot/src/boot.component.ts @@ -0,0 +1,32 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Bootstrapper} from './bootstrapper'; +import {Component, Application, CoreBindings} from '@loopback/core'; +import {inject, BindingScope} from '@loopback/context'; +import {ControllerBooter} from './booters'; + +/** + * BootComponent 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 { + // Export a list of default booters in the component so they get bound + // automatically when this component is mounted. + booters = [ControllerBooter]; + + /** + * + * @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(CoreBindings.BOOTSTRAPPER) + .toClass(Bootstrapper) + .inScope(BindingScope.SINGLETON); + } +} diff --git a/packages/boot/src/bootstrapper.ts b/packages/boot/src/bootstrapper.ts new file mode 100644 index 000000000000..075b75ffa7b3 --- /dev/null +++ b/packages/boot/src/bootstrapper.ts @@ -0,0 +1,111 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Context, inject, resolveList} from '@loopback/context'; +import { + BootOptions, + BOOTER_PHASES, + CoreBindings, + Application, +} from '@loopback/core'; +import {resolve} from 'path'; +import {BootBindings} from './keys'; + +import * as debugModule from 'debug'; +const debug = debugModule('loopback:boot:bootstrapper'); + +/** + * The Bootstrapper class provides the `boot` function that is responsible for + * finding and executing the Booters in an application based on given options. + * + * NOTE: Bootstrapper should be bound as a SINGLETON so it can be cached as + * it does not maintain any state of it's own. + * + * @param app Appliaction instance + */ +export class Bootstrapper { + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) private app: Application, + ) {} + + /** + * Function is responsible for calling all registered Booter classes that + * are bound to the Application instance. Each phase of an instance must + * complete before the next phase is started. + * @param {BootOptions} bootOptions Options for boot. Bound for Booters to + * receive via Dependency Injection. + * @param {Context} [ctx] Optional Context to use to resolve bindings. This is + * primarily useful when running app.boot() again but with different settings + * (in particular phases) such as 'start' / 'stop'. Using a returned Context from + * a previous boot call allows DI to retrieve the same instances of Booters previously + * used as they are bound using a CONTEXT scope. This is important as Booter instances + * may maintain state. + */ + async boot(bootOptions: BootOptions, ctx?: Context): Promise { + if (!bootOptions.projectRoot) { + throw new Error( + `No projectRoot provided for boot. Call boot({projectRoot: 'path'}) with projectRoot set.`, + ); + } + + const bootCtx = ctx || new Context(this.app); + + // Bind booters passed in as a part of BootOptions + if (bootOptions.booters) this.app.booter(bootOptions.booters); + + // Resolve path to projectRoot + bootOptions.projectRoot = resolve(bootOptions.projectRoot); + + // Bind Boot Options for Booters + bootCtx.bind(BootBindings.BOOT_OPTIONS).to(bootOptions); + + // Determine the phases to be run. If a user set a phases filter, those + // are selected otherwise we run the default phases (BOOTER_PHASES). + const phases = + bootOptions.filter && bootOptions.filter.phases + ? bootOptions.filter.phases + : BOOTER_PHASES; + + // Find booters registered to the BOOTERS_TAG by getting the bindings + const bindings = bootCtx.findByTag(CoreBindings.BOOTER_TAG); + + // Prefix length. +1 because of `.` => 'booters.' + const prefix_length = CoreBindings.BOOTER_PREFIX.length + 1; + + // Determing the booters to be run. If a user set a booters filter (class + // names of booters that should be run), that is the value, otherwise it + // is all the registered booters by default. + const names = + bootOptions.filter && bootOptions.filter.booters + ? bootOptions.filter.booters + : bindings.map(binding => binding.key.slice(prefix_length)); + + // Filter bindings by names + const filteredBindings = bindings.filter(binding => + names.includes(binding.key.slice(prefix_length)), + ); + + // Resolve Booter Instances + const booterInsts = await resolveList(filteredBindings, binding => + bootCtx.get(binding.key), + ); + + // Run phases of booters + for (const phase of phases) { + for (const inst of booterInsts) { + const instName = inst.constructor.name; + if (inst[phase]) { + debug(`${instName} phase: ${phase} starting.`); + await inst[phase](); + debug(`${instName} phase: ${phase} complete.`); + } else { + debug(`${instName} phase: ${phase} not implemented.`); + } + } + } + + return bootCtx; + } +} diff --git a/packages/boot/src/index.ts b/packages/boot/src/index.ts new file mode 100644 index 000000000000..c5e04c9299b5 --- /dev/null +++ b/packages/boot/src/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './booters'; +export * from './bootstrapper'; +export * from './boot.component'; +export * from './keys'; diff --git a/packages/boot/src/keys.ts b/packages/boot/src/keys.ts new file mode 100644 index 000000000000..fd975a1fec9e --- /dev/null +++ b/packages/boot/src/keys.ts @@ -0,0 +1,14 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Namespace for core binding keys + */ +export namespace BootBindings { + /** + * Binding key for Boot configuration + */ + export const BOOT_OPTIONS = 'boot.options'; +} diff --git a/packages/boot/test/unit/boot.component.unit.ts b/packages/boot/test/unit/boot.component.unit.ts new file mode 100644 index 000000000000..f1ea03705554 --- /dev/null +++ b/packages/boot/test/unit/boot.component.unit.ts @@ -0,0 +1,37 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {Application, CoreBindings} from '@loopback/core'; +import { + BootComponent, + BootBindings, + Bootstrapper, + ControllerBooter, +} from '../../index'; + +describe('boot.component unit tests', () => { + let app: Application; + + beforeEach(getApp); + + it('binds BootStrapper class', async () => { + const bootstrapper = await app.get(CoreBindings.BOOTSTRAPPER); + expect(bootstrapper).to.be.an.instanceOf(Bootstrapper); + }); + + it('ControllerBooter is bound as a booter by default', async () => { + app.bind(BootBindings.BOOT_OPTIONS).to({projectRoot: __dirname}); + const ctrlBooter = await app.get( + `${CoreBindings.BOOTER_PREFIX}.ControllerBooter`, + ); + expect(ctrlBooter).to.be.an.instanceOf(ControllerBooter); + }); + + function getApp() { + app = new Application(); + app.component(BootComponent); + } +}); diff --git a/packages/boot/test/unit/bootstrapper.unit.ts b/packages/boot/test/unit/bootstrapper.unit.ts new file mode 100644 index 000000000000..a658be049b10 --- /dev/null +++ b/packages/boot/test/unit/bootstrapper.unit.ts @@ -0,0 +1,108 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {Application, Booter, CoreBindings} from '@loopback/core'; +import {Bootstrapper} from '../../index'; + +describe('boot-strapper unit tests', () => { + let app: Application; + let bootstrapper: Bootstrapper; + const booterKey = `${CoreBindings.BOOTER_PREFIX}.TestBooter`; + const booterKey2 = `${CoreBindings.BOOTER_PREFIX}.TestBooter2`; + + beforeEach(getApplication); + beforeEach(getBootStrapper); + + it('finds and runs registered booters', async () => { + const ctx = await bootstrapper.boot({projectRoot: __dirname}); + const booterInst = await ctx.get(booterKey); + expect(booterInst.configureCalled).to.be.True(); + expect(booterInst.loadCalled).to.be.True(); + }); + + it('binds booters passed in BootOptions', async () => { + const ctx = await bootstrapper.boot({ + projectRoot: __dirname, + booters: [TestBooter2], + }); + const booterInst2 = await ctx.get(booterKey2); + expect(booterInst2).to.be.instanceof(TestBooter2); + expect(booterInst2.configureCalled).to.be.True(); + }); + + it('no booters run when BootOptions.filter.booters is []', async () => { + const ctx = await bootstrapper.boot({ + projectRoot: __dirname, + filter: {booters: []}, + }); + const booterInst = await ctx.get(booterKey); + expect(booterInst.configureCalled).to.be.False(); + expect(booterInst.loadCalled).to.be.False(); + }); + + it('only runs booters passed in via BootOptions.filter.booters', async () => { + const ctx = await bootstrapper.boot({ + projectRoot: __dirname, + booters: [TestBooter2], + filter: {booters: ['TestBooter2']}, + }); + const booterInst = await ctx.get(booterKey); + const booterInst2 = await ctx.get(booterKey2); + expect(booterInst.configureCalled).to.be.False(); + expect(booterInst.loadCalled).to.be.False(); + expect(booterInst2.configureCalled).to.be.True(); + }); + + it('only runs phases passed in via BootOptions.filter.phases', async () => { + const ctx = await bootstrapper.boot({ + projectRoot: __dirname, + filter: {phases: ['configure']}, + }); + const booterInst = await ctx.get(booterKey); + expect(booterInst.configureCalled).to.be.True(); + expect(booterInst.loadCalled).to.be.False(); + }); + + /** + * Sets 'app' as a new instance of Application. Registers TestBooter as a booter. + */ + function getApplication() { + app = new Application(); + app.booter(TestBooter); + } + + /** + * Sets 'bootstrapper' as a new instance of a Bootstrapper + */ + function getBootStrapper() { + bootstrapper = new Bootstrapper(app); + } + + /** + * A TestBooter for testing purposes. Implements configure and load. + */ + class TestBooter implements Booter { + configureCalled = false; + loadCalled = false; + async configure() { + this.configureCalled = true; + } + + async load() { + this.loadCalled = true; + } + } + + /** + * A TestBooter for testing purposes. Implements configure. + */ + class TestBooter2 implements Booter { + configureCalled = false; + async configure() { + this.configureCalled = true; + } + } +}); From 516733e7a2517284edbee5e568d5eaaa3da6bc74 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Tue, 6 Feb 2018 19:28:51 -0500 Subject: [PATCH 03/12] feat(boot): controller booter implementation --- .../boot/src/booters/base-artifact.booter.ts | 118 ++++++++++++++++++ packages/boot/src/booters/booter-utils.ts | 62 +++++++++ .../boot/src/booters/controller.booter.ts | 51 ++++++++ packages/boot/src/booters/index.ts | 8 ++ .../controller.booter.acceptance.ts | 64 ++++++++++ packages/boot/test/fixtures/application.ts | 26 ++++ packages/boot/test/fixtures/empty.artifact.ts | 6 + .../boot/test/fixtures/multiple.artifact.ts | 24 ++++ .../controller.booter.integration.ts | 56 +++++++++ .../unit/booters/base-artifact.booter.unit.ts | 79 ++++++++++++ .../test/unit/booters/booter-utils.unit.ts | 109 ++++++++++++++++ .../unit/booters/controller.booter.unit.ts | 69 ++++++++++ 12 files changed, 672 insertions(+) create mode 100644 packages/boot/src/booters/base-artifact.booter.ts create mode 100644 packages/boot/src/booters/booter-utils.ts create mode 100644 packages/boot/src/booters/controller.booter.ts create mode 100644 packages/boot/src/booters/index.ts create mode 100644 packages/boot/test/acceptance/controller.booter.acceptance.ts create mode 100644 packages/boot/test/fixtures/application.ts create mode 100644 packages/boot/test/fixtures/empty.artifact.ts create mode 100644 packages/boot/test/fixtures/multiple.artifact.ts create mode 100644 packages/boot/test/integration/controller.booter.integration.ts create mode 100644 packages/boot/test/unit/booters/base-artifact.booter.unit.ts create mode 100644 packages/boot/test/unit/booters/booter-utils.unit.ts create mode 100644 packages/boot/test/unit/booters/controller.booter.unit.ts diff --git a/packages/boot/src/booters/base-artifact.booter.ts b/packages/boot/src/booters/base-artifact.booter.ts new file mode 100644 index 000000000000..f96fa212efa6 --- /dev/null +++ b/packages/boot/src/booters/base-artifact.booter.ts @@ -0,0 +1,118 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Booter, BootOptions} from '@loopback/core'; +import {Constructor} from '@loopback/context'; +import {discoverFiles, loadClassesFromFiles} from './booter-utils'; + +/** + * 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 set the 'options' + * property to a object of ArtifactOptions type in the constructor after the + * 'super' call. + * + * 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 + * + * @param bootConfig BootStrapper Config Options + * @property options Options being used by the Booter (set in construtor) + * @property projectRoot Project root relative to which all other paths are resovled + * @property dirs Directories to look for an artifact in + * @property extensions File extensions to look for to match an artifact (this is a convention based booter) + * @property glob Glob pattern to use to discover artifacts. Takes precedence over (dirs + extensions) + * @property discovered List of files discovered by the Booter that matched artifact requirements + * @property classes List of exported classes discovered in the files + */ +export class BaseArtifactBooter implements Booter { + /** + * Options being used by the Booter. + */ + options: ArtifactOptions; + projectRoot: string; + dirs: string[]; + extensions: string[]; + glob: string; + discovered: string[]; + // tslint:disable-next-line:no-any + classes: Array>; + discoverFiles = discoverFiles; + protected loadClassesFromFiles = loadClassesFromFiles; + + constructor(bootConfig: BootOptions) { + this.projectRoot = bootConfig.projectRoot; + } + + /** + * 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 = Array.isArray(this.options.dirs) + ? this.options.dirs + : [this.options.dirs]; + + this.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() { + this.discovered = await this.discoverFiles(this.glob, this.projectRoot); + } + + /** + * 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 = await this.loadClassesFromFiles(this.discovered); + } +} + +/** + * Type definition for ArtifactOptions. These are the options supported by + * this Booter. + * + * @param dirs String / String Array of directories to check for artifacts. + * Paths must be relative. Defaults to ['controllers'] + * @param extensions String / String Array of file extensions to match artifact + * files in dirs. Defaults to ['.controller.js'] + * @param nested Boolean to control if artifact discovery should check nested + * folders or not. Default to true + * @param glob Optional. A `glob` string to use when searching for files. This takes + * precendence over other options. + */ +export type ArtifactOptions = { + dirs: string | string[]; + extensions: string | string[]; + nested: boolean; + glob?: string; +}; diff --git a/packages/boot/src/booters/booter-utils.ts b/packages/boot/src/booters/booter-utils.ts new file mode 100644 index 000000000000..69ba50b74b71 --- /dev/null +++ b/packages/boot/src/booters/booter-utils.ts @@ -0,0 +1,62 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor} from '@loopback/context'; +import {promisify} from 'util'; +const glob = promisify(require('glob')); + +/** + * 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 {string[]} Array of discovered files + */ +export async function discoverFiles( + pattern: string, + root: string, +): Promise { + return await 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 {boolean} True if target is a class. False otherwise. + */ +// tslint:disable-next-line:no-any +export function isClass(target: Constructor): boolean { + 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 + * @returns {Promise>>} An array of Class Construtors from a file + */ +// tslint:disable-next-line:no-any +export async function loadClassesFromFiles( + files: string[], + // tslint:disable-next-line:no-any +): Promise>> { + // tslint:disable-next-line:no-any + const classes: Array> = []; + files.forEach(file => { + const data = require(file); + Object.keys(data).forEach(cls => { + if (isClass(data[cls])) { + classes.push(data[cls]); + } + }); + }); + + return classes; +} diff --git a/packages/boot/src/booters/controller.booter.ts b/packages/boot/src/booters/controller.booter.ts new file mode 100644 index 000000000000..824a9d35d85f --- /dev/null +++ b/packages/boot/src/booters/controller.booter.ts @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {CoreBindings, Application, BootOptions} from '@loopback/core'; +import {inject} from '@loopback/context'; +import {BootBindings} from '../keys'; +import {BaseArtifactBooter, ArtifactOptions} from './base-artifact.booter'; + +/** + * A class that extends BaseArtifactBooter to boot the 'Controller' artifact type. + * Discovered controllers are bound using `app.controller()`. + * + * Supported phases: configure, discover, load + * + * @param app Application instance + * @param bootConfig BootStrapper Config Options + */ +export class ControllerBooter extends BaseArtifactBooter { + constructor( + @inject(BootBindings.BOOT_OPTIONS) public bootConfig: BootOptions, + @inject(CoreBindings.APPLICATION_INSTANCE) public app: Application, + ) { + super(bootConfig); + // Set Controller Booter Options if passed in via bootConfig + this.options = bootConfig.controllers + ? Object.assign({}, ControllerDefaults, bootConfig.controllers) + : Object.assign({}, ControllerDefaults); + } + + /** + * Uses super method to get a list of Artifact classes. Boot each class by + * binding it to the application using `app.controller(controller);`. + */ + async load() { + await super.load(); + this.classes.forEach(cls => { + this.app.controller(cls); + }); + } +} + +/** + * Default ArtifactOptions for a ControllerBooter. + */ +export const ControllerDefaults: ArtifactOptions = { + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, +}; diff --git a/packages/boot/src/booters/index.ts b/packages/boot/src/booters/index.ts new file mode 100644 index 000000000000..d0dc48ab2bda --- /dev/null +++ b/packages/boot/src/booters/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './base-artifact.booter'; +export * from './booter-utils'; +export * from './controller.booter'; diff --git a/packages/boot/test/acceptance/controller.booter.acceptance.ts b/packages/boot/test/acceptance/controller.booter.acceptance.ts new file mode 100644 index 000000000000..f0fdff1180d7 --- /dev/null +++ b/packages/boot/test/acceptance/controller.booter.acceptance.ts @@ -0,0 +1,64 @@ +// Copyright IBM Corp. 2013,2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Client, createClientForHandler, TestSandbox} from '@loopback/testlab'; +import {RestServer} from '@loopback/rest'; +import {resolve} from 'path'; +import {ControllerBooterApp} from '../fixtures/application'; + +describe('controller booter acceptance tests', () => { + let app: ControllerBooterApp; + const SANDBOX_PATH = resolve(__dirname, '../../.sandbox'); + const sandbox = new TestSandbox(SANDBOX_PATH); + + beforeEach(resetSandbox); + beforeEach(getApp); + + afterEach(stopApp); + + it('binds controllers using ControllerDefaults and REST endpoints work', async () => { + await app.boot(); + await app.start(); + + const server: RestServer = await app.getServer(RestServer); + const client: Client = createClientForHandler(server.handleHttp); + + // 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()'); + }); + + async function getApp() { + await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js')); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/application.js.map'), + ); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/multiple.artifact.js'), + 'controllers/multiple.controller.js', + ); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/multiple.artifact.js.map'), + 'controllers/multiple.artifact.js.map', + ); + + const BooterApp = require(resolve(SANDBOX_PATH, 'application.js')) + .ControllerBooterApp; + + app = new BooterApp(); + } + + async function stopApp() { + try { + await app.stop(); + } catch (err) { + console.log(`Stopping the app threw an error: ${err}`); + } + } + + async function resetSandbox() { + await sandbox.reset(); + } +}); diff --git a/packages/boot/test/fixtures/application.ts b/packages/boot/test/fixtures/application.ts new file mode 100644 index 000000000000..41434bf10b50 --- /dev/null +++ b/packages/boot/test/fixtures/application.ts @@ -0,0 +1,26 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {RestApplication} from '@loopback/rest'; +import {ApplicationConfig, BootOptions} from '@loopback/core'; +import {BootComponent} from '../../index'; + +export class ControllerBooterApp extends RestApplication { + constructor(options?: ApplicationConfig) { + super(options); + this.component(BootComponent); + } + + async boot(): Promise { + const bootOptions: BootOptions = { + projectRoot: __dirname, + }; + await super.boot(bootOptions); + } + + async start() { + await super.start(); + } +} diff --git a/packages/boot/test/fixtures/empty.artifact.ts b/packages/boot/test/fixtures/empty.artifact.ts new file mode 100644 index 000000000000..b2c8233645ed --- /dev/null +++ b/packages/boot/test/fixtures/empty.artifact.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// THIS FILE IS INTENTIONALLY LEFT EMPTY! diff --git a/packages/boot/test/fixtures/multiple.artifact.ts b/packages/boot/test/fixtures/multiple.artifact.ts new file mode 100644 index 000000000000..2bf32f1f0124 --- /dev/null +++ b/packages/boot/test/fixtures/multiple.artifact.ts @@ -0,0 +1,24 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {get} from '@loopback/openapi-v2'; + +export class ControllerOne { + @get('/one') + one() { + return 'ControllerOne.one()'; + } +} + +export class ControllerTwo { + @get('/two') + two() { + return 'ControllerTwo.two()'; + } +} + +export function hello() { + return 'hello world'; +} diff --git a/packages/boot/test/integration/controller.booter.integration.ts b/packages/boot/test/integration/controller.booter.integration.ts new file mode 100644 index 000000000000..c837532cad1c --- /dev/null +++ b/packages/boot/test/integration/controller.booter.integration.ts @@ -0,0 +1,56 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect, TestSandbox} from '@loopback/testlab'; +import {CoreBindings} from '@loopback/core'; +import {resolve} from 'path'; +import {ControllerBooterApp} from '../fixtures/application'; + +describe('controller booter integration tests', () => { + const SANDBOX_PATH = resolve(__dirname, '../../.sandbox'); + const sandbox = new TestSandbox(SANDBOX_PATH); + let app: ControllerBooterApp; + + beforeEach(resetSandbox); + beforeEach(getApp); + + it('boots controllers when app.boot() is called', async () => { + const expectedBindings = [ + `${CoreBindings.CONTROLLERS_PREFIX}.ControllerOne`, + `${CoreBindings.CONTROLLERS_PREFIX}.ControllerTwo`, + ]; + + await app.boot(); + + const bindings = app + .findByTag(CoreBindings.CONTROLLERS_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/application.js.map'), + ); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/multiple.artifact.js'), + 'controllers/multiple.controller.js', + ); + await sandbox.copyFile( + resolve(__dirname, '../fixtures/multiple.artifact.js.map'), + 'controllers/multiple.artifact.js.map', + ); + + const BooterApp = require(resolve(SANDBOX_PATH, 'application.js')) + .ControllerBooterApp; + + app = new BooterApp(); + } + + async function resetSandbox() { + await sandbox.reset(); + } +}); diff --git a/packages/boot/test/unit/booters/base-artifact.booter.unit.ts b/packages/boot/test/unit/booters/base-artifact.booter.unit.ts new file mode 100644 index 000000000000..4fc18083eb28 --- /dev/null +++ b/packages/boot/test/unit/booters/base-artifact.booter.unit.ts @@ -0,0 +1,79 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BaseArtifactBooter} from '../../../index'; +import {expect} from '@loopback/testlab'; +import {resolve} from 'path'; + +describe('base-artifact booter unit tests', () => { + let booterInst: BaseArtifactBooter; + + beforeEach(getBaseBooter); + + it('sets the projectRoot property from bootConfig', () => { + booterInst = new BaseArtifactBooter({projectRoot: __dirname}); + expect(booterInst.projectRoot).to.equal(__dirname); + }); + + describe('configure()', () => { + const options = { + dirs: ['test', 'test2'], + extensions: ['.test.js', 'test2.js'], + nested: false, + }; + + it(`sets 'dirs' / 'extensions' properties as an array if a string`, async () => { + booterInst.options = {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 () => { + booterInst.options = options; + 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 () => { + booterInst.options = Object.assign({}, 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'; + booterInst.options = Object.assign({}, options, {glob: expected}); + await booterInst.configure(); + expect(booterInst.glob).to.equal(expected); + }); + }); + + describe('discover()', () => { + it(`sets 'discovered' property`, async () => { + // 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 () => { + 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); + }); + }); + + async function getBaseBooter() { + booterInst = new BaseArtifactBooter({projectRoot: __dirname}); + } +}); diff --git a/packages/boot/test/unit/booters/booter-utils.unit.ts b/packages/boot/test/unit/booters/booter-utils.unit.ts new file mode 100644 index 000000000000..a9cbe25be6fb --- /dev/null +++ b/packages/boot/test/unit/booters/booter-utils.unit.ts @@ -0,0 +1,109 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {discoverFiles, isClass, loadClassesFromFiles} from '../../../index'; +import {resolve} from 'path'; +import {TestSandbox, expect} from '@loopback/testlab'; + +describe('booter-utils unit tests', () => { + const SANDBOX_PATH = resolve(__dirname, '../../../.sandbox'); + const sandbox = new TestSandbox(SANDBOX_PATH); + + beforeEach(resetSandbox); + + 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/empty.artifact.js.map'), + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/multiple.artifact.js'), + 'nested/multiple.artifact.js', + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/multiple.artifact.js.map'), + 'nested/multiple.artifact.js.map', + ); + } + }); + + 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'), + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/multiple.artifact.js.map'), + ); + const files = [resolve(SANDBOX_PATH, 'multiple.artifact.js')]; + const NUM_CLASSES = 2; // Number of classes in above file + + const classes = await loadClassesFromFiles(files); + 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'), + ); + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/empty.artifact.js.map'), + ); + const files = [resolve(SANDBOX_PATH, 'empty.artifact.js')]; + + const classes = await loadClassesFromFiles(files); + 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)).to.eventually.throw(); + }); + }); + + async function resetSandbox() { + await sandbox.reset(); + } +}); diff --git a/packages/boot/test/unit/booters/controller.booter.unit.ts b/packages/boot/test/unit/booters/controller.booter.unit.ts new file mode 100644 index 000000000000..0559266936fa --- /dev/null +++ b/packages/boot/test/unit/booters/controller.booter.unit.ts @@ -0,0 +1,69 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect, TestSandbox} from '@loopback/testlab'; +import {Application, CoreBindings} from '@loopback/core'; +import {ControllerBooter, ControllerDefaults} from '../../../index'; +import {resolve} from 'path'; + +describe('controller booter unit tests', () => { + const SANDBOX_PATH = resolve(__dirname, '../../../.sandbox'); + const sandbox = new TestSandbox(SANDBOX_PATH); + + let app: Application; + + beforeEach(resetSandbox); + beforeEach(getApp); + + it(`constructor uses ControllerDefaults for 'options' if none are given`, () => { + const booterInst = new ControllerBooter({projectRoot: SANDBOX_PATH}, app); + expect(booterInst.options).to.deepEqual(ControllerDefaults); + }); + + it('overrides defaults with provided options and uses defaults for rest', () => { + const options = { + dirs: ['test', 'test2'], + extensions: ['.ext1', 'ext2'], + }; + const expected = Object.assign({}, options, { + nested: ControllerDefaults.nested, + }); + + const booterInst = new ControllerBooter( + {projectRoot: SANDBOX_PATH, controllers: options}, + app, + ); + expect(booterInst.options).to.deepEqual(expected); + }); + + it('binds controllers during load phase', async () => { + const expected = [ + `${CoreBindings.CONTROLLERS_PREFIX}.ControllerOne`, + `${CoreBindings.CONTROLLERS_PREFIX}.ControllerTwo`, + ]; + await sandbox.copyFile( + resolve(__dirname, '../../fixtures/multiple.artifact.js'), + ); + const booterInst = new ControllerBooter({projectRoot: SANDBOX_PATH}, app); + 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 ctrls = app.findByTag(CoreBindings.CONTROLLERS_TAG); + const keys = ctrls.map(binding => binding.key); + expect(keys).to.have.lengthOf(NUM_CLASSES); + expect(keys.sort()).to.eql(expected.sort()); + }); + + function getApp() { + app = new Application(); + } + + async function resetSandbox() { + await sandbox.reset(); + } +}); From 8395fdb2298957037c0aea1fdfc4fd895cd92252 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Tue, 6 Feb 2018 19:30:09 -0500 Subject: [PATCH 04/12] feat(core): add boot, booters, and related interfaces / types --- packages/core/docs.json | 1 + packages/core/src/application.ts | 72 +++++++++++++- packages/core/src/booter.ts | 75 ++++++++++++++ packages/core/src/component.ts | 9 ++ packages/core/src/index.ts | 1 + packages/core/src/keys.ts | 9 ++ packages/core/test/unit/application.test.ts | 102 ++++++++++++++++---- 7 files changed, 245 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/booter.ts diff --git a/packages/core/docs.json b/packages/core/docs.json index f4634ab5e5c0..01c4befdb150 100644 --- a/packages/core/docs.json +++ b/packages/core/docs.json @@ -2,6 +2,7 @@ "content": [ "index.ts", "src/application.ts", + "src/booter.ts", "src/component.ts", "src/index.ts", "src/keys.ts", diff --git a/packages/core/src/application.ts b/packages/core/src/application.ts index 9c437d45f57f..6542dd9b66fd 100644 --- a/packages/core/src/application.ts +++ b/packages/core/src/application.ts @@ -7,6 +7,7 @@ import {Context, Binding, BindingScope, Constructor} from '@loopback/context'; import {Server} from './server'; import {Component, mountComponent} from './component'; import {CoreBindings} from './keys'; +import {Booter, BootOptions} from './booter'; /** * Application is the container for various types of artifacts, such as @@ -28,10 +29,10 @@ export class Application extends Context { /** * Register a controller class with this application. * - * @param controllerCtor {Function} The controller class - * (constructor function). + * @param {Function} controllerCtor The controller class + * (constructor function) * @param {string=} name Optional controller name, default to the class name - * @return {Binding} The newly created binding, you can use the reference to + * @returns {Binding} The newly created binding, you can use the reference to * further modify the binding, e.g. lock the value to prevent further * modifications. * @@ -43,9 +44,70 @@ export class Application extends Context { */ controller(controllerCtor: ControllerClass, name?: string): Binding { name = name || controllerCtor.name; - return this.bind(`controllers.${name}`) + return this.bind(`${CoreBindings.CONTROLLERS_PREFIX}.${name}`) .toClass(controllerCtor) - .tag('controller'); + .tag(CoreBindings.CONTROLLERS_TAG); + } + + /** + * Register a booter class / array of classes with this application. + * + * @param {Function | Function[]} booterCls The booter class (constructor function). + * @param {string=} name Optional booter name, defaults to the class name. + * Ignored is cls is an Array and the name defaults to the class name. + * @returns {Binding | Binding[]} The newly created binding(s), you can use the + * reference to further modify the binding, e.g. lock the value to prevent + * further modifications. + * + * ```ts + * class MyBooter implements Booter {} + * app.booter(MyBooter); + * ``` + */ + booter(booterCls: Constructor, name?: string): Binding; + booter(booterCls: Constructor[]): Binding[]; + booter( + booterCls: Constructor | Constructor[], + name?: string, + // tslint:disable-next-line:no-any + ): any { + if (Array.isArray(booterCls)) { + return booterCls.map(cls => this._bindBooter(cls)); + } else { + return this._bindBooter(booterCls, name); + } + } + + /** + * + * @param booterCls A Booter Class + * @param {string} name Name the Booter Class should be bound to + * @returns {Binding} The newly created Binding + */ + private _bindBooter( + booterCls: Constructor, + name?: string, + ): Binding { + name = name || booterCls.name; + return this.bind(`${CoreBindings.BOOTER_PREFIX}.${name}`) + .toClass(booterCls) + .inScope(BindingScope.CONTEXT) + .tag(CoreBindings.BOOTER_TAG); + } + + /** + * Function is responsible for calling all registered Booter classes that + * are bound to the Application instance. Each phase of an instance must + * complete before the next phase is started. + * @param {BootOptions} bootOptions Options to use to boot the Application + */ + async boot(bootOptions: BootOptions): Promise { + try { + const bootstrapper = await this.get(CoreBindings.BOOTSTRAPPER); + await bootstrapper.boot(bootOptions); + } catch (err) { + console.warn(`No bootstrapper was bound to ${CoreBindings.BOOTSTRAPPER}`); + } } /** diff --git a/packages/core/src/booter.ts b/packages/core/src/booter.ts new file mode 100644 index 000000000000..418bbe1f2702 --- /dev/null +++ b/packages/core/src/booter.ts @@ -0,0 +1,75 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/core +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor} from '@loopback/context'; + +/** + * Defines the requirements to implement a Booter for LoopBack applications: + * configure() : Promise + * discover() : Promise + * load(): Promise + * + * A Booter will run through the above methods in order. + * + * @export + * @interface Booter + */ +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']; + +/** + * Type Object for Options passed into .boot() + * + * @property projectRoot Root of project. All other artifacts are resolved relative to this + * @property booters An array of booters to bind to the application before running bootstrapper + * @property filter.booters An array of booters that should be run by the bootstrapper + * @property filter.phases An array of phases that should be run + */ +export type BootOptions = { + /** + * Root of the project. All other artifacts are resolved relative to this. + */ + projectRoot: string; + /** + * Optional array of Booter Classes to bind to the application before running bootstrapper. + */ + booters?: Constructor[]; + /** + * Filter Object for Bootstrapper + */ + filter?: { + /** + * Names of booters that should be run by Bootstrapper + */ + booters?: string[]; + /** + * Names of phases that should be run by Bootstrapper + */ + phases?: string[]; + }; + /** + * Additional Properties + */ + // tslint:disable-next-line:no-any + [prop: string]: any; +}; diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index f1553c2fbcf3..351e05e836ce 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -6,6 +6,7 @@ import {Constructor, Provider, BoundValue} from '@loopback/context'; import {Server} from './server'; import {Application, ControllerClass} from './application'; +import {Booter} from './booter'; /** * A map of name/class pairs for binding providers @@ -27,6 +28,10 @@ export interface Component { * A map of name/class pairs for binding providers */ providers?: ProviderMap; + /** + * An array of booter classes + */ + booters?: Constructor[]; /** * A map of name/class pairs for servers */ @@ -66,4 +71,8 @@ export function mountComponent(app: Application, component: Component) { app.server(component.servers[serverKey], serverKey); } } + + if (component.booters) { + app.booter(component.booters); + } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cf9d5f64358a..e6c5a8089968 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,3 +12,4 @@ export {Server} from './server'; export * from './application'; export * from './component'; export * from './keys'; +export * from './booter'; diff --git a/packages/core/src/keys.ts b/packages/core/src/keys.ts index cdf5d9339d93..d280e51dc434 100644 --- a/packages/core/src/keys.ts +++ b/packages/core/src/keys.ts @@ -23,6 +23,15 @@ export namespace CoreBindings { */ export const SERVERS = 'servers'; + // Binding Constant prefixes / tags + export const CONTROLLERS_PREFIX = 'controllers'; + export const CONTROLLERS_TAG = 'controller'; + + // Key for Binding the BootStrapper Class + export const BOOTSTRAPPER = 'application.bootstrapper'; + export const BOOTER_TAG = 'booter'; + export const BOOTER_PREFIX = 'booters'; + // controller /** * Binding key for the controller class resolved in the current request diff --git a/packages/core/test/unit/application.test.ts b/packages/core/test/unit/application.test.ts index 5fd3ece0ccc3..42bbdb774ec4 100644 --- a/packages/core/test/unit/application.test.ts +++ b/packages/core/test/unit/application.test.ts @@ -4,16 +4,23 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {Application, Server, Component} from '../../index'; -import {Context, Constructor} from '@loopback/context'; +import { + Application, + Server, + Component, + CoreBindings, + Booter, + BootOptions, +} from '../..'; +import {Context, Constructor, BindingScope} from '@loopback/context'; describe('Application', () => { + let app: Application; + beforeEach(givenApp); + describe('controller binding', () => { - let app: Application; class MyController {} - beforeEach(givenApp); - it('binds a controller', () => { const binding = app.controller(MyController); expect(Array.from(binding.tags)).to.containEql('controller'); @@ -27,21 +34,53 @@ describe('Application', () => { expect(binding.key).to.equal('controllers.my-controller'); expect(findKeysByTag(app, 'controller')).to.containEql(binding.key); }); + }); - function givenApp() { - app = new Application(); - } + describe('boot function', () => { + const bootOptions: BootOptions = {projectRoot: __dirname}; + + it('calls .boot() if a BootComponent is bound', async () => { + app + .bind(CoreBindings.BOOTSTRAPPER) + .toClass(FakeBootComponent) + .inScope(BindingScope.SINGLETON); + await app.boot(bootOptions); + const bootComponent = await app.get(CoreBindings.BOOTSTRAPPER); + expect(bootComponent.bootCalled).to.be.True(); + }); + }); + + describe('booter binding', () => { + it('binds a booter', async () => { + const binding = app.booter(TestBooter); + + expect(Array.from(binding.tags)).to.containEql('booter'); + expect(binding.key).to.equal(`${CoreBindings.BOOTER_PREFIX}.TestBooter`); + expect(findKeysByTag(app, CoreBindings.BOOTER_TAG)).to.containEql( + binding.key, + ); + }); + + it('binds an array of booters', async () => { + const bindings = app.booter([TestBooter, TestBooter2]); + const keys = bindings.map(binding => binding.key); + const expected = [ + `${CoreBindings.BOOTER_PREFIX}.TestBooter`, + `${CoreBindings.BOOTER_PREFIX}.TestBooter2`, + ]; + expect(keys.sort()).to.eql(expected.sort()); + expect(findKeysByTag(app, CoreBindings.BOOTER_TAG).sort()).to.eql( + keys.sort(), + ); + }); }); describe('component binding', () => { - let app: Application; class MyController {} class MyComponent implements Component { controllers = [MyController]; } - beforeEach(givenApp); - it('binds a component', () => { app.component(MyComponent); expect(findKeysByTag(app, 'component')).to.containEql( @@ -56,14 +95,16 @@ describe('Application', () => { ); }); - function givenApp() { - app = new Application(); - } + it('binds a booter from a component', () => { + app.component(FakeBooterComponent); + expect(findKeysByTag(app, CoreBindings.BOOTER_TAG)).to.containEql( + `${CoreBindings.BOOTER_PREFIX}.TestBooter`, + ); + }); }); describe('server binding', () => { it('defaults to constructor name', async () => { - const app = new Application(); const binding = app.server(FakeServer); expect(Array.from(binding.tags)).to.containEql('server'); const result = await app.getServer(FakeServer.name); @@ -71,7 +112,6 @@ describe('Application', () => { }); it('allows custom name', async () => { - const app = new Application(); const name = 'customName'; app.server(FakeServer, name); const result = await app.getServer(name); @@ -79,7 +119,7 @@ describe('Application', () => { }); it('allows binding of multiple servers as an array', async () => { - const app = new Application(); + app = new Application(); const bindings = app.servers([FakeServer, AnotherServer]); expect(Array.from(bindings[0].tags)).to.containEql('server'); expect(Array.from(bindings[1].tags)).to.containEql('server'); @@ -92,7 +132,7 @@ describe('Application', () => { describe('start', () => { it('starts all injected servers', async () => { - const app = new Application(); + app = new Application(); app.component(FakeComponent); await app.start(); @@ -103,7 +143,7 @@ describe('Application', () => { }); it('does not attempt to start poorly named bindings', async () => { - const app = new Application(); + app = new Application(); app.component(FakeComponent); // The app.start should not attempt to start this binding. @@ -113,6 +153,10 @@ describe('Application', () => { }); }); + function givenApp() { + app = new Application(); + } + function findKeysByTag(ctx: Context, tag: string | RegExp) { return ctx.findByTag(tag).map(binding => binding.key); } @@ -130,6 +174,14 @@ class FakeComponent implements Component { } } +class FakeBootComponent implements Component { + bootCalled = false; + + async boot(options: BootOptions) { + this.bootCalled = true; + } +} + class FakeServer extends Context implements Server { running: boolean = false; constructor() { @@ -145,3 +197,15 @@ class FakeServer extends Context implements Server { } class AnotherServer extends FakeServer {} + +class TestBooter implements Booter { + async configure() {} +} + +class TestBooter2 implements Booter { + async configure() {} +} + +class FakeBooterComponent implements Component { + booters = [TestBooter]; +} From 0a9898f5cc2a3ed408db9c81a3ae3058d524e7d6 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Tue, 6 Feb 2018 19:31:38 -0500 Subject: [PATCH 05/12] refactor(example-getting-started): use @loopback/boot --- .../loopback4-example-getting-started/.npmrc | 1 + .../CHANGELOG.md | 100 ++++++++++++ .../loopback4-example-getting-started/LICENSE | 25 +++ .../README.md | 10 ++ .../config/datasources.json | 5 + .../data/db.json | 17 ++ .../docs/1-prerequisites-and-setup.md | 30 ++++ .../docs/2-scaffold-app.md | 13 ++ .../docs/3-add-legacy-juggler.md | 54 +++++++ .../docs/4-todo-model.md | 104 ++++++++++++ .../docs/5-datasource.md | 34 ++++ .../docs/6-repository.md | 28 ++++ .../docs/7-controller.md | 65 ++++++++ .../docs/8-putting-it-together.md | 54 +++++++ .../index.d.ts | 6 + .../index.js | 11 ++ .../index.ts | 8 + .../package.json | 48 ++++++ .../src/application.ts | 47 ++++++ .../src/controllers/index.ts | 6 + .../src/controllers/todo.controller.ts | 74 +++++++++ .../src/datasources/db.datasource.ts | 28 ++++ .../src/index.ts | 19 +++ .../src/models/index.ts | 6 + .../src/models/todo.model.ts | 57 +++++++ .../src/repositories/index.ts | 6 + .../src/repositories/todo.repository.ts | 17 ++ .../test/acceptance/application.test.ts | 127 +++++++++++++++ .../test/helpers.ts | 36 +++++ .../unit/controllers/todo.controller.test.ts | 150 ++++++++++++++++++ .../tsconfig.build.json | 8 + packages/example-getting-started/package.json | 1 + .../src/application.ts | 16 +- packages/example-getting-started/src/index.ts | 1 + .../test/acceptance/application.test.ts | 1 + 35 files changed, 1205 insertions(+), 8 deletions(-) create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/README.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/index.js create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/index.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/package.json create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts create mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc b/packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md b/packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md new file mode 100644 index 000000000000..62b8dbeb62d9 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md @@ -0,0 +1,100 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + + +## [1.0.1-alpha.7](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.6...@loopback/example-getting-started@1.0.1-alpha.7) (2018-02-15) + + +### Bug Fixes + +* **example-getting-started:** remove juggler warning ([86139f6](https://github.com/strongloop/loopback-next/commit/86139f6)) +* **example-getting-started:** use sinon from testlab ([#984](https://github.com/strongloop/loopback-next/issues/984)) ([09fc791](https://github.com/strongloop/loopback-next/commit/09fc791)) + + + + + +## [1.0.1-alpha.6](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.5...@loopback/example-getting-started@1.0.1-alpha.6) (2018-02-07) + + +### Bug Fixes + +* **build:** fix tslint config and slipped violations ([22f8e05](https://github.com/strongloop/loopback-next/commit/22f8e05)) +* **example-getting-started:** update readme to use RestApplication ([#961](https://github.com/strongloop/loopback-next/issues/961)) ([b3e2c0e](https://github.com/strongloop/loopback-next/commit/b3e2c0e)) +* **example-getting-started:** use RestApplication ([#955](https://github.com/strongloop/loopback-next/issues/955)) ([3829878](https://github.com/strongloop/loopback-next/commit/3829878)) +* use parameter level decorators for openapi params ([c29dd19](https://github.com/strongloop/loopback-next/commit/c29dd19)) + + +### build + +* drop dist6 related targets ([#945](https://github.com/strongloop/loopback-next/issues/945)) ([a2368ce](https://github.com/strongloop/loopback-next/commit/a2368ce)) + + +### BREAKING CHANGES + +* Support for Node.js version lower than 8.0 has been dropped. +Please upgrade to the latest Node.js 8.x LTS version. + +Co-Authored-by: Taranveer Virk + + + + + +## [1.0.1-alpha.5](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.4...@loopback/example-getting-started@1.0.1-alpha.5) (2018-02-04) + + + + +**Note:** Version bump only for package @loopback/example-getting-started + + +## [1.0.1-alpha.4](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.3...@loopback/example-getting-started@1.0.1-alpha.4) (2018-01-30) + + + + +**Note:** Version bump only for package @loopback/example-getting-started + + +## [1.0.1-alpha.3](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.2...@loopback/example-getting-started@1.0.1-alpha.3) (2018-01-29) + + + + +**Note:** Version bump only for package @loopback/example-getting-started + + +## [1.0.1-alpha.2](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.1...@loopback/example-getting-started@1.0.1-alpha.2) (2018-01-26) + + + + +**Note:** Version bump only for package @loopback/example-getting-started + + +## [1.0.1-alpha.1](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.0...@loopback/example-getting-started@1.0.1-alpha.1) (2018-01-26) + + +### Bug Fixes + +* apply source-maps to test errors ([76a7f56](https://github.com/strongloop/loopback-next/commit/76a7f56)), closes [#602](https://github.com/strongloop/loopback-next/issues/602) +* make mocha self-contained with the source map support ([7c6d869](https://github.com/strongloop/loopback-next/commit/7c6d869)) + + + + + +## 1.0.1-alpha.0 (2018-01-19) + + +### Bug Fixes + +* **example-getting-started:** fix "extends" path to point to [@loopback](https://github.com/loopback)/build module ([5b37148](https://github.com/strongloop/loopback-next/commit/5b37148)) + + +### Features + +* **example-getting-started:** migrate into monorepo ([9478d8b](https://github.com/strongloop/loopback-next/commit/9478d8b)) diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE b/packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE new file mode 100644 index 000000000000..f078f3676325 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017,2018. All Rights Reserved. +Node module: @loopback/example-getting-started +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/cli/test/sandbox/loopback4-example-getting-started/README.md b/packages/cli/test/sandbox/loopback4-example-getting-started/README.md new file mode 100644 index 000000000000..c8eba4a84872 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/README.md @@ -0,0 +1,10 @@ +# @loopback/example-getting-started + +This is the basic tutorial for getting started with Loopback 4! + +### Stuck? +Check out our [Gitter channel](https://gitter.im/strongloop/loopback) and ask +for help with this tutorial! + +### Bugs/Feedback +Open an issue in this repository **OR** on [loopback-next](https://github.com/strongloop/loopback-next) and we'll take a look! diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json b/packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json new file mode 100644 index 000000000000..2737944870a4 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json @@ -0,0 +1,5 @@ +{ + "name": "ds", + "connector": "memory", + "file": "./data/db.json" +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json b/packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json new file mode 100644 index 000000000000..2cfe444d1590 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json @@ -0,0 +1,17 @@ +{ + "ids": { + "Todo": 2, + "TodoItem": 5 + }, + "models": { + "Todo": { + "1": "{\"title\":\"Take over the galaxy\",\"desc\":\"MWAHAHAHAHAHAHAHAHAHAHAHAHAMWAHAHAHAHAHAHAHAHAHAHAHAHA\",\"id\":1}" + }, + "TodoItem": { + "1": "{\"title\":\"build death star\",\"todoId\":1,\"id\":1,\"checklist\":[{\"title\":\"create death star\"},{\"title\":\"destroy alderaan\"},{\"title\":\"terrorize senate\"}]}", + "2": "{\"title\":\"destroy alderaan\",\"todoId\":1,\"id\":2,\"checklist\":[{\"title\":\"create death star\"},{\"title\":\"destroy alderaan\"},{\"title\":\"terrorize senate\"}]}", + "3": "{\"title\":\"terrorize senate\",\"todoId\":1,\"id\":3,\"checklist\":[{\"title\":\"create death star\"},{\"title\":\"destroy alderaan\"},{\"title\":\"terrorize senate\"}]}", + "4": "{\"title\":\"crush rebel scum\",\"todoId\":1,\"id\":4}" + } + } +} \ No newline at end of file diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md new file mode 100644 index 000000000000..cb75fb59965d --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md @@ -0,0 +1,30 @@ +## Prerequisites + +Before we can begin, you'll need to make sure you have some things installed: +- [Node.js](https://nodejs.org/en/) at v6.x or greater + +Additionally, this tutorial assumes that you are comfortable with +certain technologies, languages and concepts. +- JavaScript (ES6) +- [npm](https://www.npmjs.com/) +- [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) + +## Setup +1. Install the new loopback CLI toolkit. +``` +npm i -g @loopback/cli +``` +2. Download the "getting-started" application. +``` +lb4 example getting-started +``` + +3. Switch to the directory and install dependencies. +``` +cd loopback-example-getting-started && npm i +``` + +4. Start the app! +``` +npm start +``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md new file mode 100644 index 000000000000..edb3a160adfc --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md @@ -0,0 +1,13 @@ +### Create your app scaffolding + +Install the `@loopback/cli` package. This will give you the command-line +toolkit that can generate a basic REST app for you. +`npm i -g @loopback/cli` + +Next, navigate to whichever directory you'd like to create your new project +and run `lb4`. Follow the prompts to generate your application. For this +tutorial, when prompted with the options for selecting things like whether or +not to enable certain project features (loopback's build, tslint, mocha, etc.), +leave them all enabled. + + diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md new file mode 100644 index 000000000000..e44d07de19f2 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md @@ -0,0 +1,54 @@ +### Adding Legacy Juggler Capabilities + +Jump into the directory for your new application. You'll see a folder structure +similar to this: +``` +dist\ +node_modules\ +src\ + controllers\ + ping.controller.ts + README.md + repositories\ + README.md + application.ts + index.ts +test\ + mocha.opts + ping.controller.test.ts + README.md +index.js +index.d.ts +index.ts +``` + +The application template comes with a controller, and some default wireup in +`src/application.ts` that handles the basic configuration for your application. +For this tutorial, we won't need `ping.controller.ts` or its corresponding test, +but you can leave them in for now. + +Now that you have your setup, it's time to modify it to add in +`@loopback/repository`. Install this dependency by running +`npm i --save @loopback/repository`. + +Next, modify `src/application.ts` to change the base class of your app to use +the `RepositoryMixin`: + +#### src/application.ts +```ts +import {ApplicationConfig} from '@loopback/core'; +import {RestApplication} from '@loopback/rest'; +import {PingController} from './controllers/ping-controller'; +import {Class, Repository, RepositoryMixin} from '@loopback/repository'; + +export class TodoApplication extends RepositoryMixin(RestApplication) { + constructor(options?: ApplicationConfig) { + super(options); + this.setupControllers(); + } + + setupControllers() { + this.controller(PingController); + } +} +``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md new file mode 100644 index 000000000000..408758a746a7 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md @@ -0,0 +1,104 @@ +### Building the Todo model + +The Todo model will be the object we use both as a Data Transfer Object (DTO) on +the controller, and as a LoopBack model for the Legacy Juggler implementation. + +Create another folder in `src` called `repositories` and inside of that folder, +create two files: +- `index.ts` +- `todo.repository.ts` + +>**NOTE:** +The `index.ts` file is an export helper file; this pattern is a huge time-saver +as the number of models in your project grows, because it allows you to point +to the _directory_ when attempting to import types from a file within the target +folder. We will use this concept throughout the tutorial! +```ts +// in src/models/index.ts +export * from './foo.model'; +export * from './bar.model'; +export * from './baz.model'; + +// elsewhere... + +// with index.ts +import {Foo, Bar, Baz} from './models'; +// ...and without index.ts +import {Foo} from './models/foo.model'; +import {Bar} from './models/bar.model'; +import {Baz} from './models/baz.model'; +``` + +In our Todo model, we'll create a basic representation of what would go in +a Todo list. Our model will include: +- a unique id +- a title +- a description that details what the todo is all about +- a boolean flag for whether or not we've completed the task. + +For the Legacy Juggler to understand how to work with our model class, it +will need to extend the `Entity` type, as well as provide an override for +the `getId` function, so that it can retrieve a Todo model's ID as needed. + +Additionally, we'll define a `SchemaObject` that represents our Todo model +as an [OpenAPI Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schema-object). +This will give the OpenAPI spec builder the information it needs to describe the +Todo model on your app's OpenAPI endpoints. + +#### src/models/todo.model.ts +```ts +import {Entity, property, model} from '@loopback/repository'; +import {SchemaObject} from '@loopback/openapi-spec'; + +@model() +export class Todo extends Entity { + @property({ + type: 'number', + id: true + }) + id?: number; + + @property({ + type: 'string', + required: true + }) + title: string; + + @property({ + type: 'string' + }) + desc?: string; + + @property({ + type: 'boolean' + }) + isComplete: boolean; + + getId() { + return this.id; + } +} + +export const TodoSchema: SchemaObject = { + title: 'todoItem', + properties: { + id: { + type: 'number', + description: 'ID number of the Todo entry.' + }, + title: { + type: 'string', + description: 'Title of the Todo entry.' + }, + desc: { + type: 'number', + description: 'ID number of the Todo entry.' + }, + isComplete: { + type: 'boolean', + description: 'Whether or not the Todo entry is complete.' + } + }, + required: ['title'], +}; +``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md new file mode 100644 index 000000000000..9cc2ff210e2a --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md @@ -0,0 +1,34 @@ +### Building a Datasource + +Before we can begin constructing controllers and repositories for our +application, we need to define our datasource. + +Create a new folder in the root directory of the project called `config`, +and then inside that folder, create a `datasources.json` file. For now, we'll +be using the memory connector provided with the Juggler. + +#### config/datasources.json +```json +{ + "name": "ds", + "connector": "memory" +} +``` + +Create another folder called `datasources` in the `src` directory, and inside +that folder, create a new file called `db.datasource.ts`. + +#### src/datasources/db.datasource.ts + +```ts +import * as path from 'path'; +import * as fs from 'fs'; +import { DataSourceConstructor, juggler } from '@loopback/repository'; + +const dsConfigPath = path.resolve('config', 'datasources.json'); +const config = require(dsConfigPath); +export const db = new DataSourceConstructor(config); +``` + +This will give us a strongly-typed datasource export that we can work with to +construct our TodoRepository definition. diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md new file mode 100644 index 000000000000..b2520590985a --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md @@ -0,0 +1,28 @@ +### Create your repository + +Create another folder in `src` called `repositories` and inside of that folder, +create two files: +- `index.ts` (our export helper) +- `todo.repository.ts` + +Our TodoRepository will contain a small base class that uses the +`DefaultCrudRepository` class from `@loopback/repository` and will define the +model type we're working with, as well as its ID type. We'll also inject our +datasource so that this repository can connect to it when executing data +operations. + +#### src/repositories/todo.repository.ts +```ts +import { DefaultCrudRepository, DataSourceType } from '@loopback/repository'; +import { Todo } from '../models'; +import { inject } from '@loopback/core'; + +export class TodoRepository extends DefaultCrudRepository< + Todo, + typeof Todo.prototype.id +> { + constructor(@inject('datasource') protected datasource: DataSourceType) { + super(Todo, datasource); + } +} +``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md new file mode 100644 index 000000000000..484cd906b096 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md @@ -0,0 +1,65 @@ +### Create your controller + +Now, we'll create a controller to handle our Todo routes. Create the +`src/controllers` directory and two files inside: +- `index.ts` (export helper) +- `todo.controller.ts` + +In addition to creating the CRUD methods themselves, we'll also be adding +decorators that setup the routing as well as the expected parameters of +incoming requests. + +#### src/controllers/todo.controller.ts +```ts +import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; +import {HttpErrors} from '@loopback/rest'; +import {TodoSchema, Todo} from '../models'; +import {repository} from '@loopback/repository'; +import {TodoRepository} from '../repositories/index'; + +export class TodoController { + constructor( + @repository(TodoRepository.name) protected todoRepo: TodoRepository, + ) {} + @post('/todo') + @param.body('todo', TodoSchema) + async createTodo(todo: Todo) { + if (!todo.title) { + return Promise.reject(new HttpErrors.BadRequest('title is required')); + } + return await this.todoRepo.create(todo); + } + + @get('/todo/{id}') + @param.path.number('id') + @param.query.boolean('items') + async findTodoById(id: number, items?: boolean): Promise { + return await this.todoRepo.findById(id); + } + + @get('/todo') + async findTodos(): Promise { + return await this.todoRepo.find(); + } + + @put('/todo/{id}') + @param.path.number('id') + @param.body('todo', TodoSchema) + async replaceTodo(id: number, todo: Todo): Promise { + return await this.todoRepo.replaceById(id, todo); + } + + @patch('/todo/{id}') + @param.path.number('id') + @param.body('todo', TodoSchema) + async updateTodo(id: number, todo: Todo): Promise { + return await this.todoRepo.updateById(id, todo); + } + + @del('/todo/{id}') + @param.path.number('id') + async deleteTodo(id: number): Promise { + return await this.todoRepo.deleteById(id); + } +} +``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md new file mode 100644 index 000000000000..11bbac64ecfa --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md @@ -0,0 +1,54 @@ +### Putting it all together + +Now that we've got all of our artifacts made, let's set them up in our +application! + +We'll define a new helper function for setting up the repositories, as well +as adding in our new controller binding. + +#### src/application.ts +```ts +import {ApplicationConfig} from '@loopback/core'; +import {RestApplication} from '@loopback/rest'; +import {TodoController, PingController} from './controllers'; +import { + Class, + Repository, + RepositoryMixin, + DataSourceConstructor, +} from '@loopback/repository'; +import {db} from './datasources/db.datasource'; +import {TodoRepository} from './repositories'; + +export class TodoApplication extends RepositoryMixin(RestApplication) { + constructor(options?: ApplicationConfig) { + super(options); + this.setupControllers(); + this.setupRepositories(); + } + + setupControllers() { + this.controller(TodoController); + this.controller(PingController); + } + + setupRepositories() { + // This will allow you to test your application without needing to + // use the "real" datasource! + const datasource = + this.options && this.options.datasource + ? new DataSourceConstructor(this.options.datasource) + : db; + this.bind('datasource').to(datasource); + this.repository(TodoRepository); + } +} +``` + +### Try it out + +Now that your app is ready to go, try it out with your favourite REST client! +Start the app (`npm start`) and then make some REST requests: +- `POST /todo` with a body of `{ "title": "get the milk" }` +- `GET /todo/1` and see if you get your Todo object back. +- `PATCH /todo/1` with a body of `{ "desc": "need milk for cereal" }` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts new file mode 100644 index 000000000000..13ed083fde3a --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/index.js b/packages/cli/test/sandbox/loopback4-example-getting-started/index.js new file mode 100644 index 000000000000..bf96713b9849 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/index.js @@ -0,0 +1,11 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const application = (module.exports = require('./dist')); + +if (require.main === module) { + // Run the application + application.main(); +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/index.ts new file mode 100644 index 000000000000..a6dcc7c6d727 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any aditional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/package.json b/packages/cli/test/sandbox/loopback4-example-getting-started/package.json new file mode 100644 index 000000000000..d8321f150df9 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/package.json @@ -0,0 +1,48 @@ +{ + "name": "@loopback/example-getting-started", + "version": "1.0.1-alpha.7", + "description": "An application and tutorial on how to build with LoopBack 4.", + "private": true, + "main": "index.js", + "engines": { + "node": ">=8" + }, + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "lb-tsc es2017", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean *example-getting-started*.tgz dist package api-docs", + "prepublishOnly": "npm run build && npm run build:apidocs", + "pretest": "npm run build", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-getting-started*.tgz && tree package && npm run clean", + "start": "npm run build && node ." + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "license": "MIT", + "dependencies": { + "@loopback/context": "^4.0.0-alpha.32", + "@loopback/core": "^4.0.0-alpha.34", + "@loopback/openapi-spec": "^4.0.0-alpha.25", + "@loopback/openapi-v2": "^4.0.0-alpha.11", + "@loopback/repository": "^4.0.0-alpha.30", + "@loopback/rest": "^4.0.0-alpha.26" + }, + "devDependencies": { + "@loopback/build": "^4.0.0-alpha.13", + "@loopback/testlab": "^4.0.0-alpha.24", + "@types/node": "^8.5.9", + "source-map-support": "^0.5.2", + "typescript": "^2.5.2" + }, + "keywords": [ + "loopback", + "LoopBack", + "example", + "tutorial" + ] +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts new file mode 100644 index 000000000000..eadb5bae3f15 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {RestApplication} from '@loopback/rest'; +import {TodoController} from './controllers'; +import {TodoRepository} from './repositories'; +import {db} from './datasources/db.datasource'; +/* tslint:disable:no-unused-variable */ +// Class and Repository imports required to infer types in consuming code! +// Do not remove them! +import { + Class, + Repository, + DataSourceConstructor, + RepositoryMixin, +} from '@loopback/repository'; +/* tslint:enable:no-unused-variable */ +export class TodoApplication extends RepositoryMixin(RestApplication) { + constructor(options?: ApplicationConfig) { + super(options); + this.setupRepositories(); + this.setupControllers(); + } + + // Helper functions (just to keep things organized) + setupRepositories() { + // TODO(bajtos) Automate datasource and repo registration via @loopback/boot + // See https://github.com/strongloop/loopback-next/issues/441 + const datasource = + this.options && this.options.datasource + ? new DataSourceConstructor(this.options.datasource) + : db; + // TODO(bajtos) use app.dataSource() from @loopback/repository mixin + // (app.dataSource() is not implemented there yet) + // See https://github.com/strongloop/loopback-next/issues/743 + this.bind('datasource').to(datasource); + this.repository(TodoRepository); + } + + setupControllers() { + // TODO(bajtos) Automate controller registration via @loopback/boot + this.controller(TodoController); + } +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts new file mode 100644 index 000000000000..10f6afeed69b --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './todo.controller'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts new file mode 100644 index 000000000000..2e58030c68fa --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts @@ -0,0 +1,74 @@ +import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; +import {HttpErrors} from '@loopback/rest'; +import {TodoSchema, Todo} from '../models'; +import {repository} from '@loopback/repository'; +import {TodoRepository} from '../repositories/index'; + +export class TodoController { + // TODO(bajtos) Fix documentation (and argument names?) of @repository() + // to allow the usage below. + // See https://github.com/strongloop/loopback-next/issues/744 + constructor( + @repository(TodoRepository.name) protected todoRepo: TodoRepository, + ) {} + @post('/todo') + async createTodo( + @param.body('todo', TodoSchema) + todo: Todo, + ) { + // TODO(bajtos) This should be handled by the framework + // See https://github.com/strongloop/loopback-next/issues/118 + if (!todo.title) { + return Promise.reject(new HttpErrors.BadRequest('title is required')); + } + return await this.todoRepo.create(todo); + } + + @get('/todo/{id}') + async findTodoById( + @param.path.number('id') id: number, + @param.query.boolean('items') items?: boolean, + ): Promise { + return await this.todoRepo.findById(id); + } + + @get('/todo') + async findTodos(): Promise { + return await this.todoRepo.find(); + } + + @put('/todo/{id}') + async replaceTodo( + @param.path.number('id') id: number, + @param.body('todo', TodoSchema) + todo: Todo, + ): Promise { + // REST adapter does not coerce parameter values coming from string sources + // like path & query. As a workaround, we have to cast the value to a number + // ourselves. + // See https://github.com/strongloop/loopback-next/issues/750 + id = +id; + + return await this.todoRepo.replaceById(id, todo); + } + + @patch('/todo/{id}') + async updateTodo( + @param.path.number('id') id: number, + @param.body('todo', TodoSchema) + todo: Todo, + ): Promise { + // REST adapter does not coerce parameter values coming from string sources + // like path & query. As a workaround, we have to cast the value to a number + // ourselves. + // See https://github.com/strongloop/loopback-next/issues/750 + id = +id; + + return await this.todoRepo.updateById(id, todo); + } + + @del('/todo/{id}') + async deleteTodo(@param.path.number('id') id: number): Promise { + return await this.todoRepo.deleteById(id); + } +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts new file mode 100644 index 000000000000..7091892f1172 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as path from 'path'; +// The juggler reference must exist for consuming code to correctly infer +// type info used in the "db" export (contained in DataSourceConstructor). +// tslint:disable-next-line:no-unused-variable +import {juggler, DataSourceConstructor} from '@loopback/repository'; + +const dsConfigPath = path.resolve( + __dirname, + '..', + '..', + '..', + 'config', + 'datasources.json', +); +const config = require(dsConfigPath); + +// TODO(bajtos) Ideally, datasources should be created by @loopback/boot +// and registered with the app for dependency injection. +// However, we need to investigate how to access these datasources from +// integration tests where we don't have access to the full app object. +// For example, @loopback/boot can provide a helper function for +// performing a partial boot that creates datasources only. +export const db = new DataSourceConstructor(config); diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts new file mode 100644 index 000000000000..9d23ea75a801 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {TodoApplication} from './application'; +import {RestServer} from '@loopback/rest'; + +export async function main() { + const app = new TodoApplication(); + try { + await app.start(); + } catch (err) { + console.error(`Unable to start application: ${err}`); + } + const server = await app.getServer(RestServer); + console.log(`Server is running on port ${await server.get('rest.port')}`); + return app; +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts new file mode 100644 index 000000000000..d7c59a40ea98 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './todo.model'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts new file mode 100644 index 000000000000..43a092bd481a --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts @@ -0,0 +1,57 @@ +import {Entity, property, model} from '@loopback/repository'; +import {SchemaObject} from '@loopback/openapi-spec'; + +@model() +export class Todo extends Entity { + @property({ + type: 'number', + id: true, + }) + id?: number; + + @property({ + type: 'string', + required: true, + }) + title: string; + + @property({ + type: 'string', + }) + desc?: string; + + @property({ + type: 'boolean', + }) + isComplete: boolean; + + getId() { + return this.id; + } +} + +// TODO(bajtos) The schema should be generated from model definition +// See https://github.com/strongloop/loopback-next/issues/700 +// export const TodoSchema = createSchemaFromModel(Todo); +export const TodoSchema: SchemaObject = { + title: 'todoItem', + properties: { + id: { + type: 'number', + description: 'ID number of the Todo entry.', + }, + title: { + type: 'string', + description: 'Title of the Todo entry.', + }, + desc: { + type: 'number', + description: 'ID number of the Todo entry.', + }, + isComplete: { + type: 'boolean', + description: 'Whether or not the Todo entry is complete.', + }, + }, + required: ['title'], +}; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts new file mode 100644 index 000000000000..148a9fba5b74 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './todo.repository'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts new file mode 100644 index 000000000000..d8b2f0f8c995 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts @@ -0,0 +1,17 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/example-getting-started +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {DefaultCrudRepository, DataSourceType} from '@loopback/repository'; +import {Todo} from '../models'; +import {inject} from '@loopback/core'; + +export class TodoRepository extends DefaultCrudRepository< + Todo, + typeof Todo.prototype.id +> { + constructor(@inject('datasource') protected datasource: DataSourceType) { + super(Todo, datasource); + } +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts new file mode 100644 index 000000000000..9a9d36012dbc --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts @@ -0,0 +1,127 @@ +import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {RestServer} from '@loopback/rest'; +import {TodoApplication} from '../../src/application'; +import {TodoRepository} from '../../src/repositories/'; +import {givenTodo} from '../helpers'; +import {Todo} from '../../src/models/'; + +describe('Application', () => { + let app: TodoApplication; + let server: RestServer; + let client: supertest.SuperTest; + let todoRepo: TodoRepository; + + before(givenAnApplication); + before(givenARestServer); + before(givenTodoRepository); + before(async () => { + await app.start(); + }); + before(() => { + client = createClientForHandler(server.handleHttp); + }); + after(async () => { + await app.stop(); + }); + + it('creates a todo', async () => { + const todo = givenTodo(); + const response = await client + .post('/todo') + .send(todo) + .expect(200); + expect(response.body).to.containEql(todo); + const result = await todoRepo.findById(response.body.id); + expect(result).to.containEql(todo); + }); + + it('gets a todo by ID', async () => { + const todo = await givenTodoInstance(); + await client + .get(`/todo/${todo.id}`) + .send() + .expect(200, todo); + }); + + it('replaces the todo by ID', async () => { + const todo = await givenTodoInstance(); + const updatedTodo = givenTodo({ + title: 'DO SOMETHING AWESOME', + desc: 'It has to be something ridiculous', + isComplete: true, + }); + await client + .put(`/todo/${todo.id}`) + .send(updatedTodo) + .expect(200); + const result = await todoRepo.findById(todo.id); + expect(result).to.containEql(updatedTodo); + }); + + it('updates the todo by ID ', async () => { + const todo = await givenTodoInstance(); + const updatedTodo = givenTodo({ + title: 'DO SOMETHING AWESOME', + isComplete: true, + }); + await client + .patch(`/todo/${todo.id}`) + .send(updatedTodo) + .expect(200); + const result = await todoRepo.findById(todo.id); + expect(result).to.containEql(updatedTodo); + }); + + it('deletes the todo', async () => { + const todo = await givenTodoInstance(); + await client + .del(`/todo/${todo.id}`) + .send() + .expect(200); + try { + await todoRepo.findById(todo.id); + } catch (err) { + expect(err).to.match(/No Todo found with id/); + return; + } + throw new Error('No error was thrown!'); + }); + + /* + ============================================================================ + TEST HELPERS + These functions help simplify setup of your test fixtures so that your tests + can: + - operate on a "clean" environment each time (a fresh in-memory database) + - avoid polluting the test with large quantities of setup logic to keep + them clear and easy to read + - keep them DRY (who wants to write the same stuff over and over?) + ============================================================================ + */ + function givenAnApplication() { + app = new TodoApplication({ + rest: { + port: 0, + }, + datasource: { + connector: 'memory', + }, + }); + } + + async function givenARestServer() { + server = await app.getServer(RestServer); + } + + async function givenTodoRepository() { + // TODO(bajtos) enhance RepositoryMixin to provide repository getter + // Example usage: + // todoRepo = await app.getRepository(TodoRepository.name) + // See https://github.com/strongloop/loopback-next/issues/745 + todoRepo = (await app.get('repositories.TodoRepository')) as TodoRepository; + } + + async function givenTodoInstance(todo?: Partial) { + return await todoRepo.create(givenTodo(todo)); + } +}); diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts new file mode 100644 index 000000000000..a14e478f01f8 --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts @@ -0,0 +1,36 @@ +import {Todo} from '../src/models/index'; + +/* + ============================================================================== + HELPER FUNCTIONS + If you find yourself creating the same helper functions across different + test files, then extracting those functions into helper modules is an easy + way to reduce duplication. + + Other tips: + + - Using the super awesome Partial type in conjunction with Object.assign + means you can: + * customize the object you get back based only on what's important + to you during a particular test + * avoid writing test logic that is brittle with respect to the properties + of your object + - Making the input itself optional means you don't need to do anything special + for tests where the particular details of the input don't matter. + ============================================================================== + * + +/** + * Generate a complete Todo object for use with tests. + * @param todo A partial (or complete) Todo object. + */ +export function givenTodo(todo?: Partial) { + return Object.assign( + new Todo({ + title: 'do a thing', + desc: 'There are some things that need doing', + isComplete: false, + }), + todo, + ); +} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts new file mode 100644 index 000000000000..f86329b11b6f --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts @@ -0,0 +1,150 @@ +import {expect, sinon} from '@loopback/testlab'; +import {TodoController} from '../../../src/controllers'; +import {TodoRepository} from '../../../src/repositories'; +import {Todo} from '../../../src/models/index'; +import {givenTodo} from '../../helpers'; + +describe('TodoController', () => { + let todoRepo: TodoRepository; + + /* + ============================================================================= + METHOD STUBS + These handles give us a quick way to fake the response of our repository + without needing to wrangle fake repository objects or manage real ones + in our tests themselves. + ============================================================================= + */ + let create: sinon.SinonStub; + let findById: sinon.SinonStub; + let find: sinon.SinonStub; + let replaceById: sinon.SinonStub; + let updateById: sinon.SinonStub; + let deleteById: sinon.SinonStub; + + /* + ============================================================================= + TEST VARIABLES + Combining top-level objects with our resetRepositories method means we don't + need to duplicate several variable assignments (and generation statements) + in all of our test logic. + + NOTE: If you wanted to parallelize your test runs, you should avoid this + pattern since each of these tests is sharing references. + ============================================================================= + */ + let controller: TodoController; + let aTodo: Todo; + let aTodoWithId: Todo; + let aChangedTodo: Todo; + let aTodoList: Todo[]; + + const noError = 'No error was thrown!'; + + beforeEach(resetRepositories); + describe('createTodo', () => { + it('creates a Todo', async () => { + create.resolves(aTodoWithId); + const result = await controller.createTodo(aTodo); + expect(result).to.eql(aTodoWithId); + sinon.assert.calledWith(create, aTodo); + }); + + it('throws if the payload is missing a title', async () => { + const todo = givenTodo(); + delete todo.title; + try { + await controller.createTodo(todo); + } catch (err) { + expect(err).to.match(/title is required/); + sinon.assert.notCalled(create); + return; + } + // Repository stub should not have been called! + throw new Error(noError); + }); + }); + + describe('findTodoById', () => { + it('returns a todo if it exists', async () => { + findById.resolves(aTodoWithId); + expect(await controller.findTodoById(aTodoWithId.id as number)).to.eql( + aTodoWithId, + ); + sinon.assert.calledWith(findById, aTodoWithId.id); + }); + }); + + describe('findTodos', () => { + it('returns multiple todos if they exist', async () => { + find.resolves(aTodoList); + expect(await controller.findTodos()).to.eql(aTodoList); + sinon.assert.called(find); + }); + + it('returns empty list if no todos exist', async () => { + const expected: Todo[] = []; + find.resolves(expected); + expect(await controller.findTodos()).to.eql(expected); + sinon.assert.called(find); + }); + }); + + describe('replaceTodo', () => { + it('successfully replaces existing items', async () => { + replaceById.resolves(true); + expect( + await controller.replaceTodo(aTodoWithId.id as number, aChangedTodo), + ).to.eql(true); + sinon.assert.calledWith(replaceById, aTodoWithId.id, aChangedTodo); + }); + }); + + describe('updateTodo', () => { + it('successfully updates existing items', async () => { + updateById.resolves(true); + expect( + await controller.updateTodo(aTodoWithId.id as number, aChangedTodo), + ).to.eql(true); + sinon.assert.calledWith(updateById, aTodoWithId.id, aChangedTodo); + }); + }); + + describe('deleteTodo', () => { + it('successfully deletes existing items', async () => { + deleteById.resolves(true); + expect(await controller.deleteTodo(aTodoWithId.id as number)).to.eql( + true, + ); + sinon.assert.calledWith(deleteById, aTodoWithId.id); + }); + }); + + function resetRepositories() { + todoRepo = sinon.createStubInstance(TodoRepository); + aTodo = givenTodo(); + aTodoWithId = givenTodo({ + id: 1, + }); + aTodoList = [ + aTodoWithId, + givenTodo({ + id: 2, + title: 'so many things to do', + }), + ] as Todo[]; + aChangedTodo = givenTodo({ + id: aTodoWithId.id, + title: 'Do some important things', + }); + + // Setup CRUD fakes + create = todoRepo.create as sinon.SinonStub; + findById = todoRepo.findById as sinon.SinonStub; + find = todoRepo.find as sinon.SinonStub; + updateById = todoRepo.updateById as sinon.SinonStub; + replaceById = todoRepo.replaceById as sinon.SinonStub; + deleteById = todoRepo.deleteById as sinon.SinonStub; + controller = new TodoController(todoRepo); + } +}); diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json b/packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json new file mode 100644 index 000000000000..d9dc5d30c3df --- /dev/null +++ b/packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "./node_modules/@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +} diff --git a/packages/example-getting-started/package.json b/packages/example-getting-started/package.json index d8321f150df9..3220e66714f0 100644 --- a/packages/example-getting-started/package.json +++ b/packages/example-getting-started/package.json @@ -25,6 +25,7 @@ }, "license": "MIT", "dependencies": { + "@loopback/boot": "^4.0.0-alpha.1", "@loopback/context": "^4.0.0-alpha.32", "@loopback/core": "^4.0.0-alpha.34", "@loopback/openapi-spec": "^4.0.0-alpha.25", diff --git a/packages/example-getting-started/src/application.ts b/packages/example-getting-started/src/application.ts index eadb5bae3f15..e8e491c989e8 100644 --- a/packages/example-getting-started/src/application.ts +++ b/packages/example-getting-started/src/application.ts @@ -3,9 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ApplicationConfig} from '@loopback/core'; +import {ApplicationConfig, BootOptions} from '@loopback/core'; import {RestApplication} from '@loopback/rest'; -import {TodoController} from './controllers'; +import {BootComponent} from '@loopback/boot'; import {TodoRepository} from './repositories'; import {db} from './datasources/db.datasource'; /* tslint:disable:no-unused-variable */ @@ -21,8 +21,13 @@ import { export class TodoApplication extends RepositoryMixin(RestApplication) { constructor(options?: ApplicationConfig) { super(options); + this.component(BootComponent); this.setupRepositories(); - this.setupControllers(); + } + + async boot(): Promise { + const bootOptions: BootOptions = {projectRoot: __dirname}; + await super.boot(bootOptions); } // Helper functions (just to keep things organized) @@ -39,9 +44,4 @@ export class TodoApplication extends RepositoryMixin(RestApplication) { this.bind('datasource').to(datasource); this.repository(TodoRepository); } - - setupControllers() { - // TODO(bajtos) Automate controller registration via @loopback/boot - this.controller(TodoController); - } } diff --git a/packages/example-getting-started/src/index.ts b/packages/example-getting-started/src/index.ts index 9d23ea75a801..6e0aeca118ef 100644 --- a/packages/example-getting-started/src/index.ts +++ b/packages/example-getting-started/src/index.ts @@ -9,6 +9,7 @@ import {RestServer} from '@loopback/rest'; export async function main() { const app = new TodoApplication(); try { + await app.boot(); await app.start(); } catch (err) { console.error(`Unable to start application: ${err}`); diff --git a/packages/example-getting-started/test/acceptance/application.test.ts b/packages/example-getting-started/test/acceptance/application.test.ts index 9a9d36012dbc..8df0dc04726d 100644 --- a/packages/example-getting-started/test/acceptance/application.test.ts +++ b/packages/example-getting-started/test/acceptance/application.test.ts @@ -15,6 +15,7 @@ describe('Application', () => { before(givenARestServer); before(givenTodoRepository); before(async () => { + await app.boot(); await app.start(); }); before(() => { From a5a975c8b0acf193dcfed3a2ed0ec5b6e711aabb Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Tue, 6 Feb 2018 19:33:45 -0500 Subject: [PATCH 06/12] feat(cli): enable loopbackBoot as a option for new app projects --- packages/cli/generators/app/index.js | 14 +++++++++++ .../app/templates/src/application.ts.ejs | 23 +++++++++++++++++-- .../generators/app/templates/src/index.ts.ejs | 3 +++ .../project/templates/package.json.ejs | 3 +++ .../project/templates/package.plain.json.ejs | 3 +++ packages/cli/lib/project-generator.js | 19 ++++++++------- packages/cli/test/app.js | 1 + 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/packages/cli/generators/app/index.js b/packages/cli/generators/app/index.js index 5c97ff998a72..d101e5a7cf76 100644 --- a/packages/cli/generators/app/index.js +++ b/packages/cli/generators/app/index.js @@ -11,14 +11,28 @@ module.exports = class extends ProjectGenerator { // Note: arguments and options should be defined in the constructor. constructor(args, opts) { super(args, opts); + this.buildOptions = [ + 'tslint', + 'prettier', + 'mocha', + 'loopbackBuild', + 'loopbackBoot', + ]; } _setupGenerator() { this.projectType = 'application'; + this.option('applicationName', { type: String, description: 'Application name', }); + + this.option('loopbackBoot', { + type: Boolean, + description: 'Use @loopback/boot', + }); + return super._setupGenerator(); } diff --git a/packages/cli/generators/app/templates/src/application.ts.ejs b/packages/cli/generators/app/templates/src/application.ts.ejs index aff1d0823908..504db7c09931 100644 --- a/packages/cli/generators/app/templates/src/application.ts.ejs +++ b/packages/cli/generators/app/templates/src/application.ts.ejs @@ -1,15 +1,32 @@ -import {ApplicationConfig} from '@loopback/core'; +import {Application, ApplicationConfig<% if (project.loopbackBoot) { %>, BootOptions<% } %>} from '@loopback/core'; import {RestApplication, RestServer} from '@loopback/rest'; +<% if (project.loopbackBoot) { -%> +import {BootComponent} from '@loopback/boot'; +<% } else {-%> import {PingController} from './controllers/ping.controller'; +<% } -%> import {MySequence} from './sequence'; export class <%= project.applicationName %> extends RestApplication { constructor(options?: ApplicationConfig) { + // Allow options to replace the defined components array, if desired. super(options); - this.sequence(MySequence); +<% if (project.loopbackBoot) { -%> + this.component(BootComponent); +<% } else {-%> this.setupControllers(); +<% } -%> } +<% if (project.loopbackBoot) { -%> + async boot(): Promise { + const bootOptions: BootOptions = { + projectRoot: __dirname, + }; + await super.boot(bootOptions); + } +<% } -%> + async start() { await super.start(); @@ -19,7 +36,9 @@ export class <%= project.applicationName %> extends RestApplication { console.log(`Try http://127.0.0.1:${port}/ping`); } +<% if(!project.loopbackBoot) { -%> setupControllers() { this.controller(PingController); } +<% } -%> } diff --git a/packages/cli/generators/app/templates/src/index.ts.ejs b/packages/cli/generators/app/templates/src/index.ts.ejs index beaf7c162e2e..30c516edd6b0 100644 --- a/packages/cli/generators/app/templates/src/index.ts.ejs +++ b/packages/cli/generators/app/templates/src/index.ts.ejs @@ -7,6 +7,9 @@ export async function main(options?: ApplicationConfig) { const app = new <%= project.applicationName %>(options); try { + <% if (project.loopbackBoot) { -%> + await app.boot(); + <% } -%> await app.start(); } catch (err) { console.error(`Unable to start application: ${err}`); diff --git a/packages/cli/generators/project/templates/package.json.ejs b/packages/cli/generators/project/templates/package.json.ejs index 475ac79042e3..5484ce896c54 100644 --- a/packages/cli/generators/project/templates/package.json.ejs +++ b/packages/cli/generators/project/templates/package.json.ejs @@ -57,6 +57,9 @@ "dist" ], "dependencies": { +<% if (project.loopbackBoot) { -%> + "@loopback/boot": ">=4.0.0-alpha.1", +<% } -%> "@loopback/context": ">=4.0.0-alpha.18", <% if (project.projectType === 'application') { -%> "@loopback/core": ">=4.0.0-alpha.20", diff --git a/packages/cli/generators/project/templates/package.plain.json.ejs b/packages/cli/generators/project/templates/package.plain.json.ejs index 083522a368af..4f0fde9a5f54 100644 --- a/packages/cli/generators/project/templates/package.plain.json.ejs +++ b/packages/cli/generators/project/templates/package.plain.json.ejs @@ -57,6 +57,9 @@ "dist" ], "dependencies": { +<% if (project.loopbackBoot) { -%> + "@loopback/boot": ">=4.0.0-alpha.1", +<% } -%> "@loopback/context": ">=4.0.0-alpha.18", <% if (project.projectType === 'application') { -%> "@loopback/core": ">=4.0.0-alpha.20", diff --git a/packages/cli/lib/project-generator.js b/packages/cli/lib/project-generator.js index cd27e05ce4a0..bbb826de4e4c 100644 --- a/packages/cli/lib/project-generator.js +++ b/packages/cli/lib/project-generator.js @@ -12,6 +12,10 @@ module.exports = class ProjectGenerator extends BaseGenerator { // Note: arguments and options should be defined in the constructor. constructor(args, opts) { super(args, opts); + // The default list of build options available for a project + // This list gets shown to users to let them select the appropriate + // build settings for their project. + this.buildOptions = ['tslint', 'prettier', 'mocha', 'loopbackBuild']; } _setupGenerator() { @@ -80,15 +84,10 @@ module.exports = class ProjectGenerator extends BaseGenerator { setOptions() { this.projectInfo = {projectType: this.projectType}; - [ - 'name', - 'description', - 'outdir', - 'tslint', - 'prettier', - 'mocha', - 'loopbackBuild', - ].forEach(n => { + this.projectOptions = ['name', 'description', 'outdir'].concat( + this.buildOptions + ); + this.projectOptions.forEach(n => { if (this.options[n]) { this.projectInfo[n] = this.options[n]; } @@ -144,7 +143,7 @@ module.exports = class ProjectGenerator extends BaseGenerator { promptOptions() { if (this.shouldExit()) return false; const choices = []; - ['tslint', 'prettier', 'mocha', 'loopbackBuild'].forEach(f => { + this.buildOptions.forEach(f => { if (!this.options[f]) { choices.push({ name: 'Enable ' + f, diff --git a/packages/cli/test/app.js b/packages/cli/test/app.js index 920ed0421326..2440ebe08cef 100644 --- a/packages/cli/test/app.js +++ b/packages/cli/test/app.js @@ -31,6 +31,7 @@ describe('app-generator specfic files', () => { ); assert.fileContent('src/application.ts', /constructor\(/); assert.fileContent('src/application.ts', /async start\(/); + assert.fileContent('src/application.ts', /BootComponent/); assert.file('src/index.ts'); assert.fileContent('src/index.ts', /new MyAppApplication/); From 5eaaacc2e0ef73d346551e606d5e97d28c992810 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Thu, 8 Feb 2018 16:45:50 -0500 Subject: [PATCH 07/12] fix(cli): loopbackBoot should always be enabled and not an option --- packages/cli/generators/app/index.js | 12 ------------ .../app/templates/src/application.ts.ejs | 18 +----------------- .../project/templates/package.json.ejs | 2 -- .../project/templates/package.plain.json.ejs | 2 -- 4 files changed, 1 insertion(+), 33 deletions(-) diff --git a/packages/cli/generators/app/index.js b/packages/cli/generators/app/index.js index d101e5a7cf76..84a419d1d67b 100644 --- a/packages/cli/generators/app/index.js +++ b/packages/cli/generators/app/index.js @@ -11,13 +11,6 @@ module.exports = class extends ProjectGenerator { // Note: arguments and options should be defined in the constructor. constructor(args, opts) { super(args, opts); - this.buildOptions = [ - 'tslint', - 'prettier', - 'mocha', - 'loopbackBuild', - 'loopbackBoot', - ]; } _setupGenerator() { @@ -28,11 +21,6 @@ module.exports = class extends ProjectGenerator { description: 'Application name', }); - this.option('loopbackBoot', { - type: Boolean, - description: 'Use @loopback/boot', - }); - return super._setupGenerator(); } diff --git a/packages/cli/generators/app/templates/src/application.ts.ejs b/packages/cli/generators/app/templates/src/application.ts.ejs index 504db7c09931..e417f0ec73b5 100644 --- a/packages/cli/generators/app/templates/src/application.ts.ejs +++ b/packages/cli/generators/app/templates/src/application.ts.ejs @@ -1,31 +1,21 @@ -import {Application, ApplicationConfig<% if (project.loopbackBoot) { %>, BootOptions<% } %>} from '@loopback/core'; +import {Application, ApplicationConfig, BootOptions} from '@loopback/core'; import {RestApplication, RestServer} from '@loopback/rest'; -<% if (project.loopbackBoot) { -%> import {BootComponent} from '@loopback/boot'; -<% } else {-%> -import {PingController} from './controllers/ping.controller'; -<% } -%> import {MySequence} from './sequence'; export class <%= project.applicationName %> extends RestApplication { constructor(options?: ApplicationConfig) { // Allow options to replace the defined components array, if desired. super(options); -<% if (project.loopbackBoot) { -%> this.component(BootComponent); -<% } else {-%> - this.setupControllers(); -<% } -%> } -<% if (project.loopbackBoot) { -%> async boot(): Promise { const bootOptions: BootOptions = { projectRoot: __dirname, }; await super.boot(bootOptions); } -<% } -%> async start() { await super.start(); @@ -35,10 +25,4 @@ export class <%= project.applicationName %> extends RestApplication { console.log(`Server is running at http://127.0.0.1:${port}`); console.log(`Try http://127.0.0.1:${port}/ping`); } - -<% if(!project.loopbackBoot) { -%> - setupControllers() { - this.controller(PingController); - } -<% } -%> } diff --git a/packages/cli/generators/project/templates/package.json.ejs b/packages/cli/generators/project/templates/package.json.ejs index 5484ce896c54..0bd0043a60fb 100644 --- a/packages/cli/generators/project/templates/package.json.ejs +++ b/packages/cli/generators/project/templates/package.json.ejs @@ -57,9 +57,7 @@ "dist" ], "dependencies": { -<% if (project.loopbackBoot) { -%> "@loopback/boot": ">=4.0.0-alpha.1", -<% } -%> "@loopback/context": ">=4.0.0-alpha.18", <% if (project.projectType === 'application') { -%> "@loopback/core": ">=4.0.0-alpha.20", diff --git a/packages/cli/generators/project/templates/package.plain.json.ejs b/packages/cli/generators/project/templates/package.plain.json.ejs index 4f0fde9a5f54..cdbf8cb8b3cd 100644 --- a/packages/cli/generators/project/templates/package.plain.json.ejs +++ b/packages/cli/generators/project/templates/package.plain.json.ejs @@ -57,9 +57,7 @@ "dist" ], "dependencies": { -<% if (project.loopbackBoot) { -%> "@loopback/boot": ">=4.0.0-alpha.1", -<% } -%> "@loopback/context": ">=4.0.0-alpha.18", <% if (project.projectType === 'application') { -%> "@loopback/core": ">=4.0.0-alpha.20", From fc6d9780ecf2f7e689f616a01aa48c310b5b9ee8 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Sun, 11 Feb 2018 01:16:38 -0500 Subject: [PATCH 08/12] fix(boot): move bootOptions to ApplicationConfig --- packages/boot/src/bootstrapper.ts | 38 +++++++++++----- packages/boot/test/fixtures/application.ts | 8 ++-- packages/boot/test/unit/bootstrapper.unit.ts | 44 ++++++++++++------- .../app/templates/src/application.ts.ejs | 7 +-- .../generators/app/templates/src/index.ts.ejs | 2 - packages/core/src/application.ts | 30 ++++++++++--- packages/core/src/booter.ts | 8 ++++ packages/core/test/unit/application.test.ts | 9 ++-- .../src/application.ts | 6 +-- 9 files changed, 98 insertions(+), 54 deletions(-) diff --git a/packages/boot/src/bootstrapper.ts b/packages/boot/src/bootstrapper.ts index 075b75ffa7b3..91d010cbbbe0 100644 --- a/packages/boot/src/bootstrapper.ts +++ b/packages/boot/src/bootstrapper.ts @@ -6,6 +6,7 @@ import {Context, inject, resolveList} from '@loopback/context'; import { BootOptions, + BootExecutionOptions, BOOTER_PHASES, CoreBindings, Application, @@ -34,8 +35,11 @@ export class Bootstrapper { * Function is responsible for calling all registered Booter classes that * are bound to the Application instance. Each phase of an instance must * complete before the next phase is started. + * * @param {BootOptions} bootOptions Options for boot. Bound for Booters to - * receive via Dependency Injection. + * receive via Dependency Injection. This normally contains config for Booters. + * @param {BootExecutionOptions} execOptions Execution options for boot. These + * determine the phases and booters that are run. * @param {Context} [ctx] Optional Context to use to resolve bindings. This is * primarily useful when running app.boot() again but with different settings * (in particular phases) such as 'start' / 'stop'. Using a returned Context from @@ -43,7 +47,11 @@ export class Bootstrapper { * used as they are bound using a CONTEXT scope. This is important as Booter instances * may maintain state. */ - async boot(bootOptions: BootOptions, ctx?: Context): Promise { + async boot( + bootOptions: BootOptions, + execOptions?: BootExecutionOptions, + ctx?: Context, + ): Promise { if (!bootOptions.projectRoot) { throw new Error( `No projectRoot provided for boot. Call boot({projectRoot: 'path'}) with projectRoot set.`, @@ -53,7 +61,8 @@ export class Bootstrapper { const bootCtx = ctx || new Context(this.app); // Bind booters passed in as a part of BootOptions - if (bootOptions.booters) this.app.booter(bootOptions.booters); + if (execOptions && execOptions.booters) + this.app.booter(execOptions.booters); // Resolve path to projectRoot bootOptions.projectRoot = resolve(bootOptions.projectRoot); @@ -63,10 +72,11 @@ export class Bootstrapper { // Determine the phases to be run. If a user set a phases filter, those // are selected otherwise we run the default phases (BOOTER_PHASES). - const phases = - bootOptions.filter && bootOptions.filter.phases - ? bootOptions.filter.phases - : BOOTER_PHASES; + const phases = execOptions + ? execOptions.filter && execOptions.filter.phases + ? execOptions.filter.phases + : BOOTER_PHASES + : BOOTER_PHASES; // Find booters registered to the BOOTERS_TAG by getting the bindings const bindings = bootCtx.findByTag(CoreBindings.BOOTER_TAG); @@ -74,13 +84,19 @@ export class Bootstrapper { // Prefix length. +1 because of `.` => 'booters.' const prefix_length = CoreBindings.BOOTER_PREFIX.length + 1; + // Names of all registered booters. + const defaultBooterNames = bindings.map(binding => + binding.key.slice(prefix_length), + ); + // Determing the booters to be run. If a user set a booters filter (class // names of booters that should be run), that is the value, otherwise it // is all the registered booters by default. - const names = - bootOptions.filter && bootOptions.filter.booters - ? bootOptions.filter.booters - : bindings.map(binding => binding.key.slice(prefix_length)); + const names = execOptions + ? execOptions.filter && execOptions.filter.booters + ? execOptions.filter.booters + : defaultBooterNames + : defaultBooterNames; // Filter bindings by names const filteredBindings = bindings.filter(binding => diff --git a/packages/boot/test/fixtures/application.ts b/packages/boot/test/fixtures/application.ts index 41434bf10b50..504d11719280 100644 --- a/packages/boot/test/fixtures/application.ts +++ b/packages/boot/test/fixtures/application.ts @@ -4,20 +4,18 @@ // License text available at https://opensource.org/licenses/MIT import {RestApplication} from '@loopback/rest'; -import {ApplicationConfig, BootOptions} from '@loopback/core'; +import {ApplicationConfig} from '@loopback/core'; import {BootComponent} from '../../index'; export class ControllerBooterApp extends RestApplication { constructor(options?: ApplicationConfig) { + options = Object.assign({bootOptions: {projectRoot: __dirname}}, options); super(options); this.component(BootComponent); } async boot(): Promise { - const bootOptions: BootOptions = { - projectRoot: __dirname, - }; - await super.boot(bootOptions); + await super.boot(); } async start() { diff --git a/packages/boot/test/unit/bootstrapper.unit.ts b/packages/boot/test/unit/bootstrapper.unit.ts index a658be049b10..c6208864abb7 100644 --- a/packages/boot/test/unit/bootstrapper.unit.ts +++ b/packages/boot/test/unit/bootstrapper.unit.ts @@ -24,31 +24,39 @@ describe('boot-strapper unit tests', () => { }); it('binds booters passed in BootOptions', async () => { - const ctx = await bootstrapper.boot({ - projectRoot: __dirname, - booters: [TestBooter2], - }); + const ctx = await bootstrapper.boot( + { + projectRoot: __dirname, + }, + {booters: [TestBooter2]}, + ); const booterInst2 = await ctx.get(booterKey2); expect(booterInst2).to.be.instanceof(TestBooter2); expect(booterInst2.configureCalled).to.be.True(); }); it('no booters run when BootOptions.filter.booters is []', async () => { - const ctx = await bootstrapper.boot({ - projectRoot: __dirname, - filter: {booters: []}, - }); + const ctx = await bootstrapper.boot( + { + projectRoot: __dirname, + }, + {filter: {booters: []}}, + ); const booterInst = await ctx.get(booterKey); expect(booterInst.configureCalled).to.be.False(); expect(booterInst.loadCalled).to.be.False(); }); it('only runs booters passed in via BootOptions.filter.booters', async () => { - const ctx = await bootstrapper.boot({ - projectRoot: __dirname, - booters: [TestBooter2], - filter: {booters: ['TestBooter2']}, - }); + const ctx = await bootstrapper.boot( + { + projectRoot: __dirname, + }, + { + booters: [TestBooter2], + filter: {booters: ['TestBooter2']}, + }, + ); const booterInst = await ctx.get(booterKey); const booterInst2 = await ctx.get(booterKey2); expect(booterInst.configureCalled).to.be.False(); @@ -57,10 +65,12 @@ describe('boot-strapper unit tests', () => { }); it('only runs phases passed in via BootOptions.filter.phases', async () => { - const ctx = await bootstrapper.boot({ - projectRoot: __dirname, - filter: {phases: ['configure']}, - }); + const ctx = await bootstrapper.boot( + { + projectRoot: __dirname, + }, + {filter: {phases: ['configure']}}, + ); const booterInst = await ctx.get(booterKey); expect(booterInst.configureCalled).to.be.True(); expect(booterInst.loadCalled).to.be.False(); diff --git a/packages/cli/generators/app/templates/src/application.ts.ejs b/packages/cli/generators/app/templates/src/application.ts.ejs index e417f0ec73b5..360af8b72b8b 100644 --- a/packages/cli/generators/app/templates/src/application.ts.ejs +++ b/packages/cli/generators/app/templates/src/application.ts.ejs @@ -5,16 +5,13 @@ import {MySequence} from './sequence'; export class <%= project.applicationName %> extends RestApplication { constructor(options?: ApplicationConfig) { - // Allow options to replace the defined components array, if desired. + options = Object.assign({bootOptions: {projectRoot: __dirname}}, options); super(options); this.component(BootComponent); } async boot(): Promise { - const bootOptions: BootOptions = { - projectRoot: __dirname, - }; - await super.boot(bootOptions); + await super.boot(); } async start() { diff --git a/packages/cli/generators/app/templates/src/index.ts.ejs b/packages/cli/generators/app/templates/src/index.ts.ejs index 30c516edd6b0..b0cb07b8e74f 100644 --- a/packages/cli/generators/app/templates/src/index.ts.ejs +++ b/packages/cli/generators/app/templates/src/index.ts.ejs @@ -7,9 +7,7 @@ export async function main(options?: ApplicationConfig) { const app = new <%= project.applicationName %>(options); try { - <% if (project.loopbackBoot) { -%> await app.boot(); - <% } -%> await app.start(); } catch (err) { console.error(`Unable to start application: ${err}`); diff --git a/packages/core/src/application.ts b/packages/core/src/application.ts index 6542dd9b66fd..8e0587662538 100644 --- a/packages/core/src/application.ts +++ b/packages/core/src/application.ts @@ -8,6 +8,7 @@ import {Server} from './server'; import {Component, mountComponent} from './component'; import {CoreBindings} from './keys'; import {Booter, BootOptions} from './booter'; +import {BootExecutionOptions} from '../index'; /** * Application is the container for various types of artifacts, such as @@ -99,13 +100,26 @@ export class Application extends Context { * Function is responsible for calling all registered Booter classes that * are bound to the Application instance. Each phase of an instance must * complete before the next phase is started. - * @param {BootOptions} bootOptions Options to use to boot the Application + * + * @param {BootExecutionOptions} execOptions Options to control the boot + * process for the Application */ - async boot(bootOptions: BootOptions): Promise { - try { - const bootstrapper = await this.get(CoreBindings.BOOTSTRAPPER); - await bootstrapper.boot(bootOptions); - } catch (err) { + async boot(execOptions?: BootExecutionOptions): Promise { + // Get a instance of the BootStrapper + const bootstrapper = await this.get(CoreBindings.BOOTSTRAPPER, { + optional: true, + }); + // Since bootstrapper is optional, we check to see if instance was returned + if (bootstrapper) { + // this.options can never be undefined but TypeScript complains so we add + // a check (and throw an error message just to be safe but it should never + // be thrown). + if (this.options) { + await bootstrapper.boot(this.options.bootOptions, execOptions); + } else { + throw new Error(`Application.options need to be defined to use boot`); + } + } else { console.warn(`No bootstrapper was bound to ${CoreBindings.BOOTSTRAPPER}`); } } @@ -262,6 +276,10 @@ export class Application extends Context { * Configuration for application */ export interface ApplicationConfig { + /** + * Boot Configuration + */ + bootOptions?: BootOptions; /** * Other properties */ diff --git a/packages/core/src/booter.ts b/packages/core/src/booter.ts index 418bbe1f2702..5049a9d848a4 100644 --- a/packages/core/src/booter.ts +++ b/packages/core/src/booter.ts @@ -50,6 +50,14 @@ export type BootOptions = { * Root of the project. All other artifacts are resolved relative to this. */ projectRoot: string; + /** + * Additional Properties + */ + // tslint:disable-next-line:no-any + [prop: string]: any; +}; + +export type BootExecutionOptions = { /** * Optional array of Booter Classes to bind to the application before running bootstrapper. */ diff --git a/packages/core/test/unit/application.test.ts b/packages/core/test/unit/application.test.ts index 42bbdb774ec4..f056817b5e12 100644 --- a/packages/core/test/unit/application.test.ts +++ b/packages/core/test/unit/application.test.ts @@ -11,6 +11,7 @@ import { CoreBindings, Booter, BootOptions, + BootExecutionOptions, } from '../..'; import {Context, Constructor, BindingScope} from '@loopback/context'; @@ -37,14 +38,12 @@ describe('Application', () => { }); describe('boot function', () => { - const bootOptions: BootOptions = {projectRoot: __dirname}; - it('calls .boot() if a BootComponent is bound', async () => { app .bind(CoreBindings.BOOTSTRAPPER) .toClass(FakeBootComponent) .inScope(BindingScope.SINGLETON); - await app.boot(bootOptions); + await app.boot(); const bootComponent = await app.get(CoreBindings.BOOTSTRAPPER); expect(bootComponent.bootCalled).to.be.True(); }); @@ -154,7 +153,7 @@ describe('Application', () => { }); function givenApp() { - app = new Application(); + app = new Application({projectRoot: __dirname}); } function findKeysByTag(ctx: Context, tag: string | RegExp) { @@ -177,7 +176,7 @@ class FakeComponent implements Component { class FakeBootComponent implements Component { bootCalled = false; - async boot(options: BootOptions) { + async boot(options: BootOptions, execOptions?: BootExecutionOptions) { this.bootCalled = true; } } diff --git a/packages/example-getting-started/src/application.ts b/packages/example-getting-started/src/application.ts index e8e491c989e8..fe0f823bac16 100644 --- a/packages/example-getting-started/src/application.ts +++ b/packages/example-getting-started/src/application.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ApplicationConfig, BootOptions} from '@loopback/core'; +import {ApplicationConfig} from '@loopback/core'; import {RestApplication} from '@loopback/rest'; import {BootComponent} from '@loopback/boot'; import {TodoRepository} from './repositories'; @@ -20,14 +20,14 @@ import { /* tslint:enable:no-unused-variable */ export class TodoApplication extends RepositoryMixin(RestApplication) { constructor(options?: ApplicationConfig) { + options = Object.assign({bootOptions: {projectRoot: __dirname}}, options); super(options); this.component(BootComponent); this.setupRepositories(); } async boot(): Promise { - const bootOptions: BootOptions = {projectRoot: __dirname}; - await super.boot(bootOptions); + await super.boot(); } // Helper functions (just to keep things organized) From 6a9b0dc871c28fc940371751672e82c0b870cd61 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Sun, 11 Feb 2018 01:18:05 -0500 Subject: [PATCH 09/12] fix(boot): fix test name --- packages/boot/test/unit/bootstrapper.unit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/boot/test/unit/bootstrapper.unit.ts b/packages/boot/test/unit/bootstrapper.unit.ts index c6208864abb7..fa9f079e2c00 100644 --- a/packages/boot/test/unit/bootstrapper.unit.ts +++ b/packages/boot/test/unit/bootstrapper.unit.ts @@ -23,7 +23,7 @@ describe('boot-strapper unit tests', () => { expect(booterInst.loadCalled).to.be.True(); }); - it('binds booters passed in BootOptions', async () => { + it('binds booters passed in BootExecutionOptions', async () => { const ctx = await bootstrapper.boot( { projectRoot: __dirname, From 2a54c185e08c175421ff33405248fadfaa8d4b3c Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Mon, 12 Feb 2018 15:38:13 -0500 Subject: [PATCH 10/12] fix(cli): add comments to template --- packages/cli/generators/app/templates/src/application.ts.ejs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/generators/app/templates/src/application.ts.ejs b/packages/cli/generators/app/templates/src/application.ts.ejs index 360af8b72b8b..534a611a9e28 100644 --- a/packages/cli/generators/app/templates/src/application.ts.ejs +++ b/packages/cli/generators/app/templates/src/application.ts.ejs @@ -5,12 +5,15 @@ import {MySequence} from './sequence'; export class <%= project.applicationName %> extends RestApplication { constructor(options?: ApplicationConfig) { + // You can set Booter convetions on the BootOptions Object below options = Object.assign({bootOptions: {projectRoot: __dirname}}, options); super(options); this.component(BootComponent); } async boot(): Promise { + // You can pass in BootExecutionOptions to super.boot() here which + // controls how Boot is run. await super.boot(); } From edfe2ffa03ffa7fd2e868ef648e38ce1fb221725 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Tue, 13 Feb 2018 11:23:35 -0500 Subject: [PATCH 11/12] style(core): update style for if statement --- packages/core/src/application.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/application.ts b/packages/core/src/application.ts index 8e0587662538..ea02d45233b9 100644 --- a/packages/core/src/application.ts +++ b/packages/core/src/application.ts @@ -109,8 +109,11 @@ export class Application extends Context { const bootstrapper = await this.get(CoreBindings.BOOTSTRAPPER, { optional: true, }); + // Since bootstrapper is optional, we check to see if instance was returned - if (bootstrapper) { + if (!bootstrapper) { + console.warn(`No bootstrapper was bound to ${CoreBindings.BOOTSTRAPPER}`); + } else { // this.options can never be undefined but TypeScript complains so we add // a check (and throw an error message just to be safe but it should never // be thrown). @@ -119,8 +122,6 @@ export class Application extends Context { } else { throw new Error(`Application.options need to be defined to use boot`); } - } else { - console.warn(`No bootstrapper was bound to ${CoreBindings.BOOTSTRAPPER}`); } } From e55b7571ec4d208c6a14f38266a9211989af2dc8 Mon Sep 17 00:00:00 2001 From: Taranveer Virk Date: Thu, 15 Feb 2018 01:35:30 -0500 Subject: [PATCH 12/12] refactor: move boot related stuff from core to a mixin --- CODEOWNERS | 1 + MONOREPO.md | 1 + packages/boot/README.md | 49 +++--- packages/boot/docs.json | 2 + packages/boot/package.json | 8 + packages/boot/src/boot.component.ts | 3 +- packages/boot/src/boot.mixin.ts | 142 +++++++++++++++++ .../boot/src/booters/base-artifact.booter.ts | 63 +++----- packages/boot/src/booters/booter-utils.ts | 12 +- .../boot/src/booters/controller.booter.ts | 18 ++- packages/boot/src/bootstrapper.ts | 54 ++++--- packages/boot/src/index.ts | 2 + .../src/booter.ts => boot/src/interfaces.ts} | 38 ++++- packages/boot/src/keys.ts | 6 + packages/boot/test/fixtures/application.ts | 19 +-- .../controller.booter.integration.ts | 14 +- .../boot/test/unit/boot.component.unit.ts | 22 ++- packages/boot/test/unit/boot.mixin.unit.ts | 85 ++++++++++ .../unit/booters/base-artifact.booter.unit.ts | 8 +- .../test/unit/booters/booter-utils.unit.ts | 4 +- .../unit/booters/controller.booter.unit.ts | 20 +-- packages/boot/test/unit/bootstrapper.unit.ts | 54 +++---- .../app/templates/src/application.ts.ejs | 27 ++-- packages/cli/test/app.js | 4 +- .../loopback4-example-getting-started/.npmrc | 1 - .../CHANGELOG.md | 100 ------------ .../loopback4-example-getting-started/LICENSE | 25 --- .../README.md | 10 -- .../config/datasources.json | 5 - .../data/db.json | 17 -- .../docs/1-prerequisites-and-setup.md | 30 ---- .../docs/2-scaffold-app.md | 13 -- .../docs/3-add-legacy-juggler.md | 54 ------- .../docs/4-todo-model.md | 104 ------------ .../docs/5-datasource.md | 34 ---- .../docs/6-repository.md | 28 ---- .../docs/7-controller.md | 65 -------- .../docs/8-putting-it-together.md | 54 ------- .../index.d.ts | 6 - .../index.js | 11 -- .../index.ts | 8 - .../package.json | 48 ------ .../src/application.ts | 47 ------ .../src/controllers/index.ts | 6 - .../src/controllers/todo.controller.ts | 74 --------- .../src/datasources/db.datasource.ts | 28 ---- .../src/index.ts | 19 --- .../src/models/index.ts | 6 - .../src/models/todo.model.ts | 57 ------- .../src/repositories/index.ts | 6 - .../src/repositories/todo.repository.ts | 17 -- .../test/acceptance/application.test.ts | 127 --------------- .../test/helpers.ts | 36 ----- .../unit/controllers/todo.controller.test.ts | 150 ------------------ .../tsconfig.build.json | 8 - packages/core/docs.json | 1 - packages/core/src/application.ts | 91 +---------- packages/core/src/component.ts | 9 -- packages/core/src/index.ts | 1 - packages/core/src/keys.ts | 9 -- packages/core/test/unit/application.test.ts | 101 +++--------- .../src/application.ts | 18 +-- 62 files changed, 481 insertions(+), 1599 deletions(-) create mode 100644 packages/boot/src/boot.mixin.ts rename packages/{core/src/booter.ts => boot/src/interfaces.ts} (66%) create mode 100644 packages/boot/test/unit/boot.mixin.unit.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/README.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/index.js delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/index.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/package.json delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts delete mode 100644 packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json diff --git a/CODEOWNERS b/CODEOWNERS index e8fb812a9df1..028ba8a3e7be 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,6 +5,7 @@ * @bajtos @raymondfeng @kjdelisle packages/authentication/* @bajtos @kjdelisle +packages/boot/* @raymondfeng @virkt25 packages/build/* @bajtos @raymondfeng packages/cli/* @raymondfeng @kjdelisle @shimks packages/context/* @bajtos @raymondfeng @kjdelisle diff --git a/MONOREPO.md b/MONOREPO.md index cff408060d65..c7571a921023 100644 --- a/MONOREPO.md +++ b/MONOREPO.md @@ -13,6 +13,7 @@ The [loopback-next](https://github.com/strongloop/loopback-next) repository uses |[metadata](packages/metadata) |@loopback/metadata | Utilities to help developers implement TypeScript decorators, define/merge metadata, and inspect metadata | |[context](packages/context) |@loopback/context | Facilities to manage artifacts and their dependencies in your Node.js applications. The module exposes TypeScript/JavaScript APIs and decorators to register artifacts, declare dependencies, and resolve artifacts by keys. It also serves as an IoC container to support dependency injection. | |[core](packages/core) |@loopback/core | Define and implement core constructs such as Application and Component | +|[boot](packages/boot) |@loopback/boot | Convention based Bootstrapper and Booters | |[openapi-spec](packages/openapi-spec) |@loopback/openapi-spec | TypeScript type definitions for OpenAPI Spec/Swagger documents | |[openapi-spec-builder](packages/openapi-spec-builder) |@loopback/openapi-spec-builder | Builders to create OpenAPI (Swagger) specification documents in tests | |[openapi-v2](packages/openapi-v2) |@loopback/openapi-v2 | Decorators that annotate LoopBack artifacts with OpenAPI v2 (Swagger) metadata and utilities that transform LoopBack metadata to OpenAPI v2 (Swagger) specifications| diff --git a/packages/boot/README.md b/packages/boot/README.md index 7419d9d10898..f117767e3cfc 100644 --- a/packages/boot/README.md +++ b/packages/boot/README.md @@ -1,6 +1,6 @@ # @loopback/boot -A collection of Booters for LoopBack Applications +A convention based project Bootstrapper and Booters for LoopBack Applications # Overview @@ -11,9 +11,12 @@ phases to complete its task. An example task of a Booter may be to discover and bind all artifacts of a given type. -A BootStrapper is needed to manage the Booters and to run them. This is packaged -in BootComponent. Add `BootComponent` to your `Application` to use the default -`BootStrapper` and `Booters`. +A Bootstrapper is needed to manage the Booters and execute them. This is provided +in this package. For ease of use, everything needed is packages using a BootMixin. +This Mixin will add convenience methods such as `boot` and `booter`, as well as +properties needed for Bootstrapper such as `projectRoot`. The Mixin also adds the +`BootComponent` to your `Application` which binds the `Bootstrapper` and default +`Booters` made available by this package. ## Installation @@ -25,19 +28,19 @@ $ npm i @loopback/boot ```ts import {Application} from '@loopback/core'; -import {BootComponent} from '@loopback/boot'; -const app = new Application(); -app.component(BootComponent); - -await app.boot({ - projectRoot: __dirname, - booters: [RepositoryBooter], // Register Booters as part of call to app.boot() - controllers: { - dirs: ['ctrl'], - extensions: ['.ctrl.js'], - nested: true +import {BootMixin} from '@loopback/boot'; +class BootApp extends BootMixin(Application) {} + +const app = new BootApp(); +app.projectRoot = __dirname; +app.bootOptions = { + controlles: { + // Configure ControllerBooter Conventiones here. } -}); // Booter gets run by the Application +} + +await app.boot(); +await app.start(); ``` ### BootOptions @@ -45,11 +48,15 @@ List of Options available on BootOptions Object. |Option|Type|Description| |-|-|-| -|`projectRoot`|`string`|Absolute path to the root of the LoopBack 4 Project. **Required**| -|`booters`|`Constructor[]`|Array of Booters to bind before booting. *Optional*| -|`filter`|`Object`|An Object to filter Booters and phases for finer control over the boot process. *Optional*| -|`filter.booters`|`string[]`|Names of Booters that should be run (all other bound booters will be ignored).| -|`filter.phases`|`string[]`|Names of phases and order that they should be run in.| +|`controllers`|`ArtifactOptions`|ControllerBooter convention options| + +### ArtifactOptions + +**Add Table for ArtifactOptions** + +### BootExecOptions + +**Add Table for BootExecOptions** ## Available Booters diff --git a/packages/boot/docs.json b/packages/boot/docs.json index 267ffbd922ff..832f14a43b3a 100644 --- a/packages/boot/docs.json +++ b/packages/boot/docs.json @@ -6,8 +6,10 @@ "src/booters/controller.booter.ts", "src/booters/index.ts", "src/boot.component.ts", + "src/boot.mixin.ts", "src/bootstrapper.ts", "src/index.ts", + "src/interfaces.ts", "src/keys.ts" ], "codeSectionDepth": 4 diff --git a/packages/boot/package.json b/packages/boot/package.json index 2f8f5b2577f6..9c25bf3780f7 100644 --- a/packages/boot/package.json +++ b/packages/boot/package.json @@ -5,6 +5,9 @@ "engines": { "node": ">=8" }, + "publishConfig": { + "access": "public" + }, "scripts": { "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", "build": "npm run build:dist", @@ -20,6 +23,7 @@ "verify": "npm pack && tar xf loopback-boot*.tgz && tree package && npm run clean" }, "author": "IBM", + "copyright.owner": "IBM Corp.", "license": "MIT", "dependencies": { "@loopback/context": "^4.0.0-alpha.27", @@ -38,8 +42,12 @@ "files": [ "README.md", "index.js", + "index.js.map", "index.d.ts", "dist/src", + "dist/index.js", + "dist/index.js.map", + "dist/index.d.ts", "api-docs", "src" ], diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts index 89d9ba47a404..407f7be32383 100644 --- a/packages/boot/src/boot.component.ts +++ b/packages/boot/src/boot.component.ts @@ -7,6 +7,7 @@ import {Bootstrapper} from './bootstrapper'; import {Component, Application, CoreBindings} from '@loopback/core'; import {inject, BindingScope} from '@loopback/context'; import {ControllerBooter} from './booters'; +import {BootBindings} from './keys'; /** * BootComponent is used to export the default list of Booter's made @@ -25,7 +26,7 @@ export class BootComponent implements Component { constructor(@inject(CoreBindings.APPLICATION_INSTANCE) app: Application) { // Bound as a SINGLETON so it can be cached as it has no state app - .bind(CoreBindings.BOOTSTRAPPER) + .bind(BootBindings.BOOTSTRAPPER_KEY) .toClass(Bootstrapper) .inScope(BindingScope.SINGLETON); } diff --git a/packages/boot/src/boot.mixin.ts b/packages/boot/src/boot.mixin.ts new file mode 100644 index 000000000000..3b6d4674987b --- /dev/null +++ b/packages/boot/src/boot.mixin.ts @@ -0,0 +1,142 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor, Binding, BindingScope, Context} from '@loopback/context'; +import {Booter, BootOptions, Bootable} from './interfaces'; +import {BootComponent} from './boot.component'; +import {Bootstrapper} from './bootstrapper'; +import {BootBindings} from './keys'; + +// Binding is re-exported as Binding / Booter types are needed when consuming +// BootMixin and this allows a user to import them from the same package (UX!) +export {Binding}; + +/** + * Mixin for @loopback/boot. This Mixin provides the following: + * - Implements the Bootable Interface as follows. + * - 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) + * - 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` + * - Adds `mountComponentBooters` which binds Booters to the application from `component.booters[]` + * + * ******************** NOTE ******************** + * Trying to constrain the type of this Mixin (or any Mixin) will cause errors. + * For example, constraining this Mixin to type Application require all types using by + * Application to be imported (including it's dependencies such as ResolutionSession). + * Another issue was that if a Mixin that is type constrained is used with another Mixin + * that is not, it will result in an error. + * Example (class MyApp extends BootMixin(RepositoryMixin(Application))) {}; + ********************* END OF NOTE ******************** + */ +// tslint:disable-next-line:no-any +export function BootMixin>(superClass: T) { + return class extends superClass implements Bootable { + projectRoot: string; + bootOptions?: BootOptions; + + // tslint:disable-next-line:no-any + constructor(...args: any[]) { + super(...args); + this.component(BootComponent); + + // We Dynamically bind the Project Root and Boot Options so these values can + // be used to resolve an instance of the Bootstrapper (as they are dependencies) + this.bind(BootBindings.PROJECT_ROOT).toDynamicValue( + () => this.projectRoot, + ); + this.bind(BootBindings.BOOT_OPTIONS).toDynamicValue( + () => this.bootOptions, + ); + } + + /** + * Convenience method to call bootstrapper.boot() by resolving bootstrapper + */ + async boot(): Promise { + // Get a instance of the BootStrapper + const bootstrapper: Bootstrapper = await this.get( + BootBindings.BOOTSTRAPPER_KEY, + ); + + await bootstrapper.boot(); + } + + /** + * Given a N number of Booter Classes, this method binds them using the + * prefix and tag expected by the Bootstrapper. + * + * @param booterCls Booter classes to bind to the Application + * + * ```ts + * app.booters(MyBooter, MyOtherBooter) + * ``` + */ + booters(...booterCls: Constructor[]): Binding[] { + // tslint:disable-next-line:no-any + return booterCls.map(cls => _bindBooter((this), cls)); + } + + /** + * Override to ensure any Booter's on a Component are also mounted. + * + * @param component The component to add. + * + * ```ts + * + * export class ProductComponent { + * booters = [ControllerBooter, RepositoryBooter]; + * providers = { + * [AUTHENTICATION_STRATEGY]: AuthStrategy, + * [AUTHORIZATION_ROLE]: Role, + * }; + * }; + * + * app.component(ProductComponent); + * ``` + */ + public component(component: Constructor<{}>) { + super.component(component); + this.mountComponentBooters(component); + } + + /** + * Get an instance of a component and mount all it's + * booters. This function is intended to be used internally + * by component() + * + * @param component The component to mount booters of + */ + mountComponentBooters(component: Constructor<{}>) { + const componentKey = `components.${component.name}`; + const compInstance = this.getSync(componentKey); + + if (compInstance.booters) { + this.booters(...compInstance.booters); + } + } + }; +} + +/** + * 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 { + return ctx + .bind(`${BootBindings.BOOTER_PREFIX}.${booterCls.name}`) + .toClass(booterCls) + .inScope(BindingScope.CONTEXT) + .tag(BootBindings.BOOTER_TAG); +} diff --git a/packages/boot/src/booters/base-artifact.booter.ts b/packages/boot/src/booters/base-artifact.booter.ts index f96fa212efa6..a3777c97673c 100644 --- a/packages/boot/src/booters/base-artifact.booter.ts +++ b/packages/boot/src/booters/base-artifact.booter.ts @@ -3,9 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Booter, BootOptions} from '@loopback/core'; import {Constructor} from '@loopback/context'; import {discoverFiles, loadClassesFromFiles} from './booter-utils'; +import {Booter, ArtifactOptions} from '../interfaces'; /** * This class serves as a base class for Booters which follow a pattern of @@ -13,17 +13,17 @@ import {discoverFiles, loadClassesFromFiles} from './booter-utils'; * 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 set the 'options' - * property to a object of ArtifactOptions type in the constructor after the - * 'super' call. + * Any Booter extending this base class is expected to * - * Provide it's own logic for 'load' after calling 'await super.load()' 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 * - * @param bootConfig BootStrapper Config Options - * @property options Options being used by the Booter (set in construtor) + * @property options Options being used by the Booter * @property projectRoot Project root relative to which all other paths are resovled * @property dirs Directories to look for an artifact in * @property extensions File extensions to look for to match an artifact (this is a convention based booter) @@ -41,14 +41,7 @@ export class BaseArtifactBooter implements Booter { extensions: string[]; glob: string; discovered: string[]; - // tslint:disable-next-line:no-any - classes: Array>; - discoverFiles = discoverFiles; - protected loadClassesFromFiles = loadClassesFromFiles; - - constructor(bootConfig: BootOptions) { - this.projectRoot = bootConfig.projectRoot; - } + classes: Array>; /** * Configure the Booter by initializing the 'dirs', 'extensions' and 'glob' @@ -57,13 +50,17 @@ export class BaseArtifactBooter implements Booter { * NOTE: All properties are configured even if all aren't used. */ async configure() { - this.dirs = Array.isArray(this.options.dirs) - ? this.options.dirs - : [this.options.dirs]; + this.dirs = this.options.dirs + ? Array.isArray(this.options.dirs) + ? this.options.dirs + : [this.options.dirs] + : []; - this.extensions = Array.isArray(this.options.extensions) - ? this.options.extensions - : [this.options.extensions]; + 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('|'); @@ -81,7 +78,7 @@ export class BaseArtifactBooter implements Booter { * 'discovered' property. */ async discover() { - this.discovered = await this.discoverFiles(this.glob, this.projectRoot); + this.discovered = await discoverFiles(this.glob, this.projectRoot); } /** @@ -93,26 +90,6 @@ export class BaseArtifactBooter implements Booter { * and then process the artifact classes as appropriate. */ async load() { - this.classes = await this.loadClassesFromFiles(this.discovered); + this.classes = await loadClassesFromFiles(this.discovered); } } - -/** - * Type definition for ArtifactOptions. These are the options supported by - * this Booter. - * - * @param dirs String / String Array of directories to check for artifacts. - * Paths must be relative. Defaults to ['controllers'] - * @param extensions String / String Array of file extensions to match artifact - * files in dirs. Defaults to ['.controller.js'] - * @param nested Boolean to control if artifact discovery should check nested - * folders or not. Default to true - * @param glob Optional. A `glob` string to use when searching for files. This takes - * precendence over other options. - */ -export type ArtifactOptions = { - dirs: string | string[]; - extensions: string | string[]; - nested: boolean; - glob?: string; -}; diff --git a/packages/boot/src/booters/booter-utils.ts b/packages/boot/src/booters/booter-utils.ts index 69ba50b74b71..a7ca3c6ebabc 100644 --- a/packages/boot/src/booters/booter-utils.ts +++ b/packages/boot/src/booters/booter-utils.ts @@ -27,8 +27,7 @@ export async function discoverFiles( * @param target The function to check if it's a class or not. * @returns {boolean} True if target is a class. False otherwise. */ -// tslint:disable-next-line:no-any -export function isClass(target: Constructor): boolean { +export function isClass(target: Constructor<{}>): boolean { return ( typeof target === 'function' && target.toString().indexOf('class') === 0 ); @@ -40,15 +39,12 @@ export function isClass(target: Constructor): boolean { * 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 - * @returns {Promise>>} An array of Class Construtors from a file + * @returns {Promise>>} An array of Class Construtors from a file */ -// tslint:disable-next-line:no-any export async function loadClassesFromFiles( files: string[], - // tslint:disable-next-line:no-any -): Promise>> { - // tslint:disable-next-line:no-any - const classes: Array> = []; +): Promise>> { + const classes: Array> = []; files.forEach(file => { const data = require(file); Object.keys(data).forEach(cls => { diff --git a/packages/boot/src/booters/controller.booter.ts b/packages/boot/src/booters/controller.booter.ts index 824a9d35d85f..3fae6b26dc23 100644 --- a/packages/boot/src/booters/controller.booter.ts +++ b/packages/boot/src/booters/controller.booter.ts @@ -3,10 +3,11 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {CoreBindings, Application, BootOptions} from '@loopback/core'; +import {CoreBindings, Application} from '@loopback/core'; import {inject} from '@loopback/context'; +import {ArtifactOptions} from '../interfaces'; +import {BaseArtifactBooter} from './base-artifact.booter'; import {BootBindings} from '../keys'; -import {BaseArtifactBooter, ArtifactOptions} from './base-artifact.booter'; /** * A class that extends BaseArtifactBooter to boot the 'Controller' artifact type. @@ -15,18 +16,19 @@ import {BaseArtifactBooter, ArtifactOptions} from './base-artifact.booter'; * Supported phases: configure, discover, load * * @param app Application instance - * @param bootConfig BootStrapper Config Options + * @param projectRoot Root of User Project relative to which all paths are resolved + * @param [bootConfig] Controller Artifact Options Object */ export class ControllerBooter extends BaseArtifactBooter { constructor( - @inject(BootBindings.BOOT_OPTIONS) public bootConfig: BootOptions, @inject(CoreBindings.APPLICATION_INSTANCE) public app: Application, + @inject(BootBindings.PROJECT_ROOT) public projectRoot: string, + @inject(`${BootBindings.BOOT_OPTIONS}#controllers`) + public controllerConfig: ArtifactOptions = {}, ) { - super(bootConfig); + super(); // Set Controller Booter Options if passed in via bootConfig - this.options = bootConfig.controllers - ? Object.assign({}, ControllerDefaults, bootConfig.controllers) - : Object.assign({}, ControllerDefaults); + this.options = Object.assign({}, ControllerDefaults, controllerConfig); } /** diff --git a/packages/boot/src/bootstrapper.ts b/packages/boot/src/bootstrapper.ts index 91d010cbbbe0..dbd79c3ea709 100644 --- a/packages/boot/src/bootstrapper.ts +++ b/packages/boot/src/bootstrapper.ts @@ -3,16 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {resolve} from 'path'; import {Context, inject, resolveList} from '@loopback/context'; +import {CoreBindings, Application} from '@loopback/core'; import { BootOptions, BootExecutionOptions, BOOTER_PHASES, - CoreBindings, - Application, -} from '@loopback/core'; -import {resolve} from 'path'; + Bootable, +} from './interfaces'; import {BootBindings} from './keys'; +import {_bindBooter} from './boot.mixin'; import * as debugModule from 'debug'; const debug = debugModule('loopback:boot:bootstrapper'); @@ -25,19 +26,30 @@ const debug = debugModule('loopback:boot:bootstrapper'); * it does not maintain any state of it's own. * * @param app Appliaction instance + * @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 */ export class Bootstrapper { constructor( - @inject(CoreBindings.APPLICATION_INSTANCE) private app: Application, - ) {} + @inject(CoreBindings.APPLICATION_INSTANCE) + private app: Application & Bootable, + @inject(BootBindings.PROJECT_ROOT) private projectRoot: string, + @inject(BootBindings.BOOT_OPTIONS) private bootOptions: BootOptions = {}, + ) { + // Resolve path to projectRoot and re-bind + this.projectRoot = resolve(this.projectRoot); + app.bind(BootBindings.PROJECT_ROOT).to(this.projectRoot); + + // This is re-bound for testing reasons where this value may be passed directly + // and needs to be propogated to the Booters via DI + app.bind(BootBindings.BOOT_OPTIONS).to(this.bootOptions); + } /** * Function is responsible for calling all registered Booter classes that * are bound to the Application instance. Each phase of an instance must * complete before the next phase is started. * - * @param {BootOptions} bootOptions Options for boot. Bound for Booters to - * receive via Dependency Injection. This normally contains config for Booters. * @param {BootExecutionOptions} execOptions Execution options for boot. These * determine the phases and booters that are run. * @param {Context} [ctx] Optional Context to use to resolve bindings. This is @@ -48,27 +60,19 @@ export class Bootstrapper { * may maintain state. */ async boot( - bootOptions: BootOptions, execOptions?: BootExecutionOptions, ctx?: Context, ): Promise { - if (!bootOptions.projectRoot) { - throw new Error( - `No projectRoot provided for boot. Call boot({projectRoot: 'path'}) with projectRoot set.`, - ); - } - const bootCtx = ctx || new Context(this.app); // Bind booters passed in as a part of BootOptions - if (execOptions && execOptions.booters) - this.app.booter(execOptions.booters); - - // Resolve path to projectRoot - bootOptions.projectRoot = resolve(bootOptions.projectRoot); - - // Bind Boot Options for Booters - bootCtx.bind(BootBindings.BOOT_OPTIONS).to(bootOptions); + // We use _bindBooter so this Class can be used without the Mixin + if (execOptions && execOptions.booters) { + execOptions.booters.forEach(booter => + // tslint:disable-next-line:no-any + _bindBooter(this.app, booter), + ); + } // Determine the phases to be run. If a user set a phases filter, those // are selected otherwise we run the default phases (BOOTER_PHASES). @@ -79,10 +83,10 @@ export class Bootstrapper { : BOOTER_PHASES; // Find booters registered to the BOOTERS_TAG by getting the bindings - const bindings = bootCtx.findByTag(CoreBindings.BOOTER_TAG); + const bindings = bootCtx.findByTag(BootBindings.BOOTER_TAG); // Prefix length. +1 because of `.` => 'booters.' - const prefix_length = CoreBindings.BOOTER_PREFIX.length + 1; + const prefix_length = BootBindings.BOOTER_PREFIX.length + 1; // Names of all registered booters. const defaultBooterNames = bindings.map(binding => diff --git a/packages/boot/src/index.ts b/packages/boot/src/index.ts index c5e04c9299b5..28ac74b3bed8 100644 --- a/packages/boot/src/index.ts +++ b/packages/boot/src/index.ts @@ -7,3 +7,5 @@ export * from './booters'; export * from './bootstrapper'; export * from './boot.component'; export * from './keys'; +export * from './boot.mixin'; +export * from './interfaces'; diff --git a/packages/core/src/booter.ts b/packages/boot/src/interfaces.ts similarity index 66% rename from packages/core/src/booter.ts rename to packages/boot/src/interfaces.ts index 5049a9d848a4..7728f72b6299 100644 --- a/packages/core/src/booter.ts +++ b/packages/boot/src/interfaces.ts @@ -3,7 +3,27 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Constructor} from '@loopback/context'; +import {Constructor, Binding} from '@loopback/context'; + +/** + * Type definition for ArtifactOptions. These are the options supported by + * this Booter. + * + * @param dirs String / String Array of directories to check for artifacts. + * Paths must be relative. Defaults to ['controllers'] + * @param extensions String / String Array of file extensions to match artifact + * files in dirs. Defaults to ['.controller.js'] + * @param nested Boolean to control if artifact discovery should check nested + * folders or not. Default to true + * @param glob Optional. A `glob` string to use when searching for files. This takes + * precendence over other options. + */ +export type ArtifactOptions = { + dirs?: string | string[]; + extensions?: string | string[]; + nested?: boolean; + glob?: string; +}; /** * Defines the requirements to implement a Booter for LoopBack applications: @@ -46,10 +66,7 @@ export const BOOTER_PHASES = ['configure', 'discover', 'load']; * @property filter.phases An array of phases that should be run */ export type BootOptions = { - /** - * Root of the project. All other artifacts are resolved relative to this. - */ - projectRoot: string; + controllers?: ArtifactOptions; /** * Additional Properties */ @@ -81,3 +98,14 @@ export type BootExecutionOptions = { // tslint:disable-next-line:no-any [prop: string]: any; }; + +/** + * Interface to describe the additions made available to an Application + * that uses BootMixin. + */ +export interface Bootable { + projectRoot: string; + bootOptions?: BootOptions; + boot(): Promise; + booters(...booterCls: Constructor[]): Binding[]; +} diff --git a/packages/boot/src/keys.ts b/packages/boot/src/keys.ts index fd975a1fec9e..ce2e334cfbc7 100644 --- a/packages/boot/src/keys.ts +++ b/packages/boot/src/keys.ts @@ -11,4 +11,10 @@ export namespace BootBindings { * Binding key for Boot configuration */ export const BOOT_OPTIONS = 'boot.options'; + export const PROJECT_ROOT = 'boot.project_root'; + + // Key for Binding the BootStrapper Class + export const BOOTSTRAPPER_KEY = 'application.bootstrapper'; + export const BOOTER_TAG = 'booter'; + export const BOOTER_PREFIX = 'booters'; } diff --git a/packages/boot/test/fixtures/application.ts b/packages/boot/test/fixtures/application.ts index 504d11719280..50df018e3543 100644 --- a/packages/boot/test/fixtures/application.ts +++ b/packages/boot/test/fixtures/application.ts @@ -5,20 +5,15 @@ import {RestApplication} from '@loopback/rest'; import {ApplicationConfig} from '@loopback/core'; -import {BootComponent} from '../../index'; -export class ControllerBooterApp extends RestApplication { +/* tslint:disable:no-unused-variable */ +// Binding and Booter imports are required to infer types for BootMixin! +import {BootMixin, Booter, Binding} from '../../index'; +/* tslint:enable:no-unused-variable */ + +export class ControllerBooterApp extends BootMixin(RestApplication) { constructor(options?: ApplicationConfig) { - options = Object.assign({bootOptions: {projectRoot: __dirname}}, options); super(options); - this.component(BootComponent); - } - - async boot(): Promise { - await super.boot(); - } - - async start() { - await super.start(); + this.projectRoot = __dirname; } } diff --git a/packages/boot/test/integration/controller.booter.integration.ts b/packages/boot/test/integration/controller.booter.integration.ts index c837532cad1c..805a8bd7ca0a 100644 --- a/packages/boot/test/integration/controller.booter.integration.ts +++ b/packages/boot/test/integration/controller.booter.integration.ts @@ -4,13 +4,17 @@ // License text available at https://opensource.org/licenses/MIT import {expect, TestSandbox} from '@loopback/testlab'; -import {CoreBindings} from '@loopback/core'; import {resolve} from 'path'; import {ControllerBooterApp} from '../fixtures/application'; describe('controller booter integration tests', () => { const SANDBOX_PATH = resolve(__dirname, '../../.sandbox'); const sandbox = new TestSandbox(SANDBOX_PATH); + + // Remnants from Refactor -- need to add these to core + const CONTROLLERS_PREFIX = 'controllers'; + const CONTROLLERS_TAG = 'controller'; + let app: ControllerBooterApp; beforeEach(resetSandbox); @@ -18,15 +22,13 @@ describe('controller booter integration tests', () => { it('boots controllers when app.boot() is called', async () => { const expectedBindings = [ - `${CoreBindings.CONTROLLERS_PREFIX}.ControllerOne`, - `${CoreBindings.CONTROLLERS_PREFIX}.ControllerTwo`, + `${CONTROLLERS_PREFIX}.ControllerOne`, + `${CONTROLLERS_PREFIX}.ControllerTwo`, ]; await app.boot(); - const bindings = app - .findByTag(CoreBindings.CONTROLLERS_TAG) - .map(b => b.key); + const bindings = app.findByTag(CONTROLLERS_TAG).map(b => b.key); expect(bindings.sort()).to.eql(expectedBindings.sort()); }); diff --git a/packages/boot/test/unit/boot.component.unit.ts b/packages/boot/test/unit/boot.component.unit.ts index f1ea03705554..bf79b780e35d 100644 --- a/packages/boot/test/unit/boot.component.unit.ts +++ b/packages/boot/test/unit/boot.component.unit.ts @@ -4,34 +4,30 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {Application, CoreBindings} from '@loopback/core'; -import { - BootComponent, - BootBindings, - Bootstrapper, - ControllerBooter, -} from '../../index'; +import {Application} from '@loopback/core'; +import {BootBindings, Bootstrapper, ControllerBooter, BootMixin} from '../../'; describe('boot.component unit tests', () => { - let app: Application; + class BootableApp extends BootMixin(Application) {} + + let app: BootableApp; beforeEach(getApp); it('binds BootStrapper class', async () => { - const bootstrapper = await app.get(CoreBindings.BOOTSTRAPPER); + const bootstrapper = await app.get(BootBindings.BOOTSTRAPPER_KEY); expect(bootstrapper).to.be.an.instanceOf(Bootstrapper); }); it('ControllerBooter is bound as a booter by default', async () => { - app.bind(BootBindings.BOOT_OPTIONS).to({projectRoot: __dirname}); const ctrlBooter = await app.get( - `${CoreBindings.BOOTER_PREFIX}.ControllerBooter`, + `${BootBindings.BOOTER_PREFIX}.ControllerBooter`, ); expect(ctrlBooter).to.be.an.instanceOf(ControllerBooter); }); function getApp() { - app = new Application(); - app.component(BootComponent); + app = new BootableApp(); + app.bind(BootBindings.PROJECT_ROOT).to(__dirname); } }); diff --git a/packages/boot/test/unit/boot.mixin.unit.ts b/packages/boot/test/unit/boot.mixin.unit.ts new file mode 100644 index 000000000000..c37d9141b5d0 --- /dev/null +++ b/packages/boot/test/unit/boot.mixin.unit.ts @@ -0,0 +1,85 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {BootMixin, Booter, BootBindings} from '../../'; +import {Application} from '@loopback/core'; + +describe('BootMxiin unit tests', () => { + let app: AppWithBootMixin; + + beforeEach(getApp); + + it('mixes into the target class', () => { + expect(app.boot).to.be.a.Function(); + expect(app.booters).to.be.a.Function(); + }); + + it('adds BootComponent to target class', () => { + const boundComponent = app.find('components.*').map(b => b.key); + expect(boundComponent).to.containEql('components.BootComponent'); + }); + + it('binds booter from app.booters()', async () => { + app.booters(TestBooter); + const booter = await app.get(`${BootBindings.BOOTER_PREFIX}.TestBooter`); + expect(booter).to.be.an.instanceOf(TestBooter); + }); + + it('binds multiple booter classes from app.booters()', async () => { + app.booters(TestBooter, AnotherTestBooter); + const booter = await app.get(`${BootBindings.BOOTER_PREFIX}.TestBooter`); + expect(booter).to.be.an.instanceOf(TestBooter); + + const anotherBooter = await app.get( + `${BootBindings.BOOTER_PREFIX}.AnotherTestBooter`, + ); + expect(anotherBooter).to.be.an.instanceOf(AnotherTestBooter); + }); + + it('binds user defined component without a booter', async () => { + class EmptyTestComponent {} + + app.component(EmptyTestComponent); + const compInstance = await app.get('components.EmptyTestComponent'); + expect(compInstance).to.be.an.instanceOf(EmptyTestComponent); + }); + + it('binds a user defined component with a booter from .component()', async () => { + class TestComponent { + booters = [TestBooter]; + } + + app.component(TestComponent); + const compInstance = await app.get('components.TestComponent'); + expect(compInstance).to.be.an.instanceOf(TestComponent); + const booterInst = await app.get( + `${BootBindings.BOOTER_PREFIX}.TestBooter`, + ); + expect(booterInst).to.be.an.instanceOf(TestBooter); + }); + + class TestBooter implements Booter { + configured = false; + + async configure() { + this.configured = true; + } + } + + class AnotherTestBooter implements Booter { + discovered = false; + + async discover() { + this.discovered = true; + } + } + + class AppWithBootMixin extends BootMixin(Application) {} + + function getApp() { + app = new AppWithBootMixin(); + } +}); diff --git a/packages/boot/test/unit/booters/base-artifact.booter.unit.ts b/packages/boot/test/unit/booters/base-artifact.booter.unit.ts index 4fc18083eb28..9ba05542f2b9 100644 --- a/packages/boot/test/unit/booters/base-artifact.booter.unit.ts +++ b/packages/boot/test/unit/booters/base-artifact.booter.unit.ts @@ -12,11 +12,6 @@ describe('base-artifact booter unit tests', () => { beforeEach(getBaseBooter); - it('sets the projectRoot property from bootConfig', () => { - booterInst = new BaseArtifactBooter({projectRoot: __dirname}); - expect(booterInst.projectRoot).to.equal(__dirname); - }); - describe('configure()', () => { const options = { dirs: ['test', 'test2'], @@ -55,6 +50,7 @@ describe('base-artifact booter unit tests', () => { describe('discover()', () => { it(`sets 'discovered' property`, async () => { + booterInst.projectRoot = __dirname; // Fake glob pattern so we get an empty array booterInst.glob = '/abc.xyz'; await booterInst.discover(); @@ -74,6 +70,6 @@ describe('base-artifact booter unit tests', () => { }); async function getBaseBooter() { - booterInst = new BaseArtifactBooter({projectRoot: __dirname}); + booterInst = new BaseArtifactBooter(); } }); diff --git a/packages/boot/test/unit/booters/booter-utils.unit.ts b/packages/boot/test/unit/booters/booter-utils.unit.ts index a9cbe25be6fb..f81d42a8527d 100644 --- a/packages/boot/test/unit/booters/booter-utils.unit.ts +++ b/packages/boot/test/unit/booters/booter-utils.unit.ts @@ -99,7 +99,9 @@ describe('booter-utils unit tests', () => { it('throws an error given a non-existent file', async () => { const files = [resolve(SANDBOX_PATH, 'fake.artifact.js')]; - expect(loadClassesFromFiles(files)).to.eventually.throw(); + expect(loadClassesFromFiles(files)).to.eventually.throw( + /Error: Cannot find module/, + ); }); }); diff --git a/packages/boot/test/unit/booters/controller.booter.unit.ts b/packages/boot/test/unit/booters/controller.booter.unit.ts index 0559266936fa..30558f8b0e5d 100644 --- a/packages/boot/test/unit/booters/controller.booter.unit.ts +++ b/packages/boot/test/unit/booters/controller.booter.unit.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {expect, TestSandbox} from '@loopback/testlab'; -import {Application, CoreBindings} from '@loopback/core'; +import {Application} from '@loopback/core'; import {ControllerBooter, ControllerDefaults} from '../../../index'; import {resolve} from 'path'; @@ -12,13 +12,16 @@ describe('controller booter unit tests', () => { const SANDBOX_PATH = resolve(__dirname, '../../../.sandbox'); const sandbox = new TestSandbox(SANDBOX_PATH); + const CONTROLLERS_PREFIX = 'controllers'; + const CONTROLLERS_TAG = 'controller'; + let app: Application; beforeEach(resetSandbox); beforeEach(getApp); it(`constructor uses ControllerDefaults for 'options' if none are given`, () => { - const booterInst = new ControllerBooter({projectRoot: SANDBOX_PATH}, app); + const booterInst = new ControllerBooter(app, SANDBOX_PATH); expect(booterInst.options).to.deepEqual(ControllerDefaults); }); @@ -31,29 +34,26 @@ describe('controller booter unit tests', () => { nested: ControllerDefaults.nested, }); - const booterInst = new ControllerBooter( - {projectRoot: SANDBOX_PATH, controllers: options}, - app, - ); + const booterInst = new ControllerBooter(app, SANDBOX_PATH, options); expect(booterInst.options).to.deepEqual(expected); }); it('binds controllers during load phase', async () => { const expected = [ - `${CoreBindings.CONTROLLERS_PREFIX}.ControllerOne`, - `${CoreBindings.CONTROLLERS_PREFIX}.ControllerTwo`, + `${CONTROLLERS_PREFIX}.ControllerOne`, + `${CONTROLLERS_PREFIX}.ControllerTwo`, ]; await sandbox.copyFile( resolve(__dirname, '../../fixtures/multiple.artifact.js'), ); - const booterInst = new ControllerBooter({projectRoot: SANDBOX_PATH}, app); + const booterInst = new ControllerBooter(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 ctrls = app.findByTag(CoreBindings.CONTROLLERS_TAG); + const ctrls = app.findByTag(CONTROLLERS_TAG); const keys = ctrls.map(binding => binding.key); expect(keys).to.have.lengthOf(NUM_CLASSES); expect(keys.sort()).to.eql(expected.sort()); diff --git a/packages/boot/test/unit/bootstrapper.unit.ts b/packages/boot/test/unit/bootstrapper.unit.ts index fa9f079e2c00..9d6e23c41073 100644 --- a/packages/boot/test/unit/bootstrapper.unit.ts +++ b/packages/boot/test/unit/bootstrapper.unit.ts @@ -4,59 +4,46 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {Application, Booter, CoreBindings} from '@loopback/core'; -import {Bootstrapper} from '../../index'; +import {Application} from '@loopback/core'; +import {Bootstrapper, Booter, BootBindings, BootMixin} from '../../index'; describe('boot-strapper unit tests', () => { - let app: Application; + class BootApp extends BootMixin(Application) {} + + let app: BootApp; let bootstrapper: Bootstrapper; - const booterKey = `${CoreBindings.BOOTER_PREFIX}.TestBooter`; - const booterKey2 = `${CoreBindings.BOOTER_PREFIX}.TestBooter2`; + const booterKey = `${BootBindings.BOOTER_PREFIX}.TestBooter`; + const booterKey2 = `${BootBindings.BOOTER_PREFIX}.TestBooter2`; beforeEach(getApplication); beforeEach(getBootStrapper); it('finds and runs registered booters', async () => { - const ctx = await bootstrapper.boot({projectRoot: __dirname}); + const ctx = await bootstrapper.boot(); const booterInst = await ctx.get(booterKey); expect(booterInst.configureCalled).to.be.True(); expect(booterInst.loadCalled).to.be.True(); }); it('binds booters passed in BootExecutionOptions', async () => { - const ctx = await bootstrapper.boot( - { - projectRoot: __dirname, - }, - {booters: [TestBooter2]}, - ); + const ctx = await bootstrapper.boot({booters: [TestBooter2]}); const booterInst2 = await ctx.get(booterKey2); expect(booterInst2).to.be.instanceof(TestBooter2); expect(booterInst2.configureCalled).to.be.True(); }); it('no booters run when BootOptions.filter.booters is []', async () => { - const ctx = await bootstrapper.boot( - { - projectRoot: __dirname, - }, - {filter: {booters: []}}, - ); + const ctx = await bootstrapper.boot({filter: {booters: []}}); const booterInst = await ctx.get(booterKey); expect(booterInst.configureCalled).to.be.False(); expect(booterInst.loadCalled).to.be.False(); }); it('only runs booters passed in via BootOptions.filter.booters', async () => { - const ctx = await bootstrapper.boot( - { - projectRoot: __dirname, - }, - { - booters: [TestBooter2], - filter: {booters: ['TestBooter2']}, - }, - ); + const ctx = await bootstrapper.boot({ + booters: [TestBooter2], + filter: {booters: ['TestBooter2']}, + }); const booterInst = await ctx.get(booterKey); const booterInst2 = await ctx.get(booterKey2); expect(booterInst.configureCalled).to.be.False(); @@ -65,12 +52,7 @@ describe('boot-strapper unit tests', () => { }); it('only runs phases passed in via BootOptions.filter.phases', async () => { - const ctx = await bootstrapper.boot( - { - projectRoot: __dirname, - }, - {filter: {phases: ['configure']}}, - ); + const ctx = await bootstrapper.boot({filter: {phases: ['configure']}}); const booterInst = await ctx.get(booterKey); expect(booterInst.configureCalled).to.be.True(); expect(booterInst.loadCalled).to.be.False(); @@ -80,15 +62,15 @@ describe('boot-strapper unit tests', () => { * Sets 'app' as a new instance of Application. Registers TestBooter as a booter. */ function getApplication() { - app = new Application(); - app.booter(TestBooter); + app = new BootApp(); + app.booters(TestBooter); } /** * Sets 'bootstrapper' as a new instance of a Bootstrapper */ function getBootStrapper() { - bootstrapper = new Bootstrapper(app); + bootstrapper = new Bootstrapper(app, __dirname); } /** diff --git a/packages/cli/generators/app/templates/src/application.ts.ejs b/packages/cli/generators/app/templates/src/application.ts.ejs index 534a611a9e28..2ec25ec71d00 100644 --- a/packages/cli/generators/app/templates/src/application.ts.ejs +++ b/packages/cli/generators/app/templates/src/application.ts.ejs @@ -1,20 +1,25 @@ import {Application, ApplicationConfig, BootOptions} from '@loopback/core'; import {RestApplication, RestServer} from '@loopback/rest'; -import {BootComponent} from '@loopback/boot'; import {MySequence} from './sequence'; -export class <%= project.applicationName %> extends RestApplication { +/* tslint:disable:no-unused-variable */ +// Binding and Booter imports are required to infer types for BootMixin! +import {BootMixin, Booter, Binding} from '@loopback/boot'; +/* tslint:enable:no-unused-variable */ + +export class <%= project.applicationName %> extends BootMixin(RestApplication) { constructor(options?: ApplicationConfig) { - // You can set Booter convetions on the BootOptions Object below - options = Object.assign({bootOptions: {projectRoot: __dirname}}, options); super(options); - this.component(BootComponent); - } - - async boot(): Promise { - // You can pass in BootExecutionOptions to super.boot() here which - // controls how Boot is run. - await super.boot(); + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = { + controllers: { + // Customize ControllerBooter Conventions here + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, + } + } } async start() { diff --git a/packages/cli/test/app.js b/packages/cli/test/app.js index 2440ebe08cef..c7500db7c599 100644 --- a/packages/cli/test/app.js +++ b/packages/cli/test/app.js @@ -27,11 +27,11 @@ describe('app-generator specfic files', () => { assert.file('src/application.ts'); assert.fileContent( 'src/application.ts', - /class MyAppApplication extends RestApplication/ + /class MyAppApplication extends BootMixin\(RestApplication/ ); assert.fileContent('src/application.ts', /constructor\(/); assert.fileContent('src/application.ts', /async start\(/); - assert.fileContent('src/application.ts', /BootComponent/); + assert.fileContent('src/application.ts', /this.projectRoot = __dirname/); assert.file('src/index.ts'); assert.fileContent('src/index.ts', /new MyAppApplication/); diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc b/packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc deleted file mode 100644 index 43c97e719a5a..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md b/packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md deleted file mode 100644 index 62b8dbeb62d9..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/CHANGELOG.md +++ /dev/null @@ -1,100 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - - -## [1.0.1-alpha.7](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.6...@loopback/example-getting-started@1.0.1-alpha.7) (2018-02-15) - - -### Bug Fixes - -* **example-getting-started:** remove juggler warning ([86139f6](https://github.com/strongloop/loopback-next/commit/86139f6)) -* **example-getting-started:** use sinon from testlab ([#984](https://github.com/strongloop/loopback-next/issues/984)) ([09fc791](https://github.com/strongloop/loopback-next/commit/09fc791)) - - - - - -## [1.0.1-alpha.6](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.5...@loopback/example-getting-started@1.0.1-alpha.6) (2018-02-07) - - -### Bug Fixes - -* **build:** fix tslint config and slipped violations ([22f8e05](https://github.com/strongloop/loopback-next/commit/22f8e05)) -* **example-getting-started:** update readme to use RestApplication ([#961](https://github.com/strongloop/loopback-next/issues/961)) ([b3e2c0e](https://github.com/strongloop/loopback-next/commit/b3e2c0e)) -* **example-getting-started:** use RestApplication ([#955](https://github.com/strongloop/loopback-next/issues/955)) ([3829878](https://github.com/strongloop/loopback-next/commit/3829878)) -* use parameter level decorators for openapi params ([c29dd19](https://github.com/strongloop/loopback-next/commit/c29dd19)) - - -### build - -* drop dist6 related targets ([#945](https://github.com/strongloop/loopback-next/issues/945)) ([a2368ce](https://github.com/strongloop/loopback-next/commit/a2368ce)) - - -### BREAKING CHANGES - -* Support for Node.js version lower than 8.0 has been dropped. -Please upgrade to the latest Node.js 8.x LTS version. - -Co-Authored-by: Taranveer Virk - - - - - -## [1.0.1-alpha.5](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.4...@loopback/example-getting-started@1.0.1-alpha.5) (2018-02-04) - - - - -**Note:** Version bump only for package @loopback/example-getting-started - - -## [1.0.1-alpha.4](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.3...@loopback/example-getting-started@1.0.1-alpha.4) (2018-01-30) - - - - -**Note:** Version bump only for package @loopback/example-getting-started - - -## [1.0.1-alpha.3](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.2...@loopback/example-getting-started@1.0.1-alpha.3) (2018-01-29) - - - - -**Note:** Version bump only for package @loopback/example-getting-started - - -## [1.0.1-alpha.2](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.1...@loopback/example-getting-started@1.0.1-alpha.2) (2018-01-26) - - - - -**Note:** Version bump only for package @loopback/example-getting-started - - -## [1.0.1-alpha.1](https://github.com/strongloop/loopback-next/compare/@loopback/example-getting-started@1.0.1-alpha.0...@loopback/example-getting-started@1.0.1-alpha.1) (2018-01-26) - - -### Bug Fixes - -* apply source-maps to test errors ([76a7f56](https://github.com/strongloop/loopback-next/commit/76a7f56)), closes [#602](https://github.com/strongloop/loopback-next/issues/602) -* make mocha self-contained with the source map support ([7c6d869](https://github.com/strongloop/loopback-next/commit/7c6d869)) - - - - - -## 1.0.1-alpha.0 (2018-01-19) - - -### Bug Fixes - -* **example-getting-started:** fix "extends" path to point to [@loopback](https://github.com/loopback)/build module ([5b37148](https://github.com/strongloop/loopback-next/commit/5b37148)) - - -### Features - -* **example-getting-started:** migrate into monorepo ([9478d8b](https://github.com/strongloop/loopback-next/commit/9478d8b)) diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE b/packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE deleted file mode 100644 index f078f3676325..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) IBM Corp. 2017,2018. All Rights Reserved. -Node module: @loopback/example-getting-started -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/cli/test/sandbox/loopback4-example-getting-started/README.md b/packages/cli/test/sandbox/loopback4-example-getting-started/README.md deleted file mode 100644 index c8eba4a84872..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# @loopback/example-getting-started - -This is the basic tutorial for getting started with Loopback 4! - -### Stuck? -Check out our [Gitter channel](https://gitter.im/strongloop/loopback) and ask -for help with this tutorial! - -### Bugs/Feedback -Open an issue in this repository **OR** on [loopback-next](https://github.com/strongloop/loopback-next) and we'll take a look! diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json b/packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json deleted file mode 100644 index 2737944870a4..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/config/datasources.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "ds", - "connector": "memory", - "file": "./data/db.json" -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json b/packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json deleted file mode 100644 index 2cfe444d1590..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/data/db.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "ids": { - "Todo": 2, - "TodoItem": 5 - }, - "models": { - "Todo": { - "1": "{\"title\":\"Take over the galaxy\",\"desc\":\"MWAHAHAHAHAHAHAHAHAHAHAHAHAMWAHAHAHAHAHAHAHAHAHAHAHAHA\",\"id\":1}" - }, - "TodoItem": { - "1": "{\"title\":\"build death star\",\"todoId\":1,\"id\":1,\"checklist\":[{\"title\":\"create death star\"},{\"title\":\"destroy alderaan\"},{\"title\":\"terrorize senate\"}]}", - "2": "{\"title\":\"destroy alderaan\",\"todoId\":1,\"id\":2,\"checklist\":[{\"title\":\"create death star\"},{\"title\":\"destroy alderaan\"},{\"title\":\"terrorize senate\"}]}", - "3": "{\"title\":\"terrorize senate\",\"todoId\":1,\"id\":3,\"checklist\":[{\"title\":\"create death star\"},{\"title\":\"destroy alderaan\"},{\"title\":\"terrorize senate\"}]}", - "4": "{\"title\":\"crush rebel scum\",\"todoId\":1,\"id\":4}" - } - } -} \ No newline at end of file diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md deleted file mode 100644 index cb75fb59965d..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/1-prerequisites-and-setup.md +++ /dev/null @@ -1,30 +0,0 @@ -## Prerequisites - -Before we can begin, you'll need to make sure you have some things installed: -- [Node.js](https://nodejs.org/en/) at v6.x or greater - -Additionally, this tutorial assumes that you are comfortable with -certain technologies, languages and concepts. -- JavaScript (ES6) -- [npm](https://www.npmjs.com/) -- [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) - -## Setup -1. Install the new loopback CLI toolkit. -``` -npm i -g @loopback/cli -``` -2. Download the "getting-started" application. -``` -lb4 example getting-started -``` - -3. Switch to the directory and install dependencies. -``` -cd loopback-example-getting-started && npm i -``` - -4. Start the app! -``` -npm start -``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md deleted file mode 100644 index edb3a160adfc..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/2-scaffold-app.md +++ /dev/null @@ -1,13 +0,0 @@ -### Create your app scaffolding - -Install the `@loopback/cli` package. This will give you the command-line -toolkit that can generate a basic REST app for you. -`npm i -g @loopback/cli` - -Next, navigate to whichever directory you'd like to create your new project -and run `lb4`. Follow the prompts to generate your application. For this -tutorial, when prompted with the options for selecting things like whether or -not to enable certain project features (loopback's build, tslint, mocha, etc.), -leave them all enabled. - - diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md deleted file mode 100644 index e44d07de19f2..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/3-add-legacy-juggler.md +++ /dev/null @@ -1,54 +0,0 @@ -### Adding Legacy Juggler Capabilities - -Jump into the directory for your new application. You'll see a folder structure -similar to this: -``` -dist\ -node_modules\ -src\ - controllers\ - ping.controller.ts - README.md - repositories\ - README.md - application.ts - index.ts -test\ - mocha.opts - ping.controller.test.ts - README.md -index.js -index.d.ts -index.ts -``` - -The application template comes with a controller, and some default wireup in -`src/application.ts` that handles the basic configuration for your application. -For this tutorial, we won't need `ping.controller.ts` or its corresponding test, -but you can leave them in for now. - -Now that you have your setup, it's time to modify it to add in -`@loopback/repository`. Install this dependency by running -`npm i --save @loopback/repository`. - -Next, modify `src/application.ts` to change the base class of your app to use -the `RepositoryMixin`: - -#### src/application.ts -```ts -import {ApplicationConfig} from '@loopback/core'; -import {RestApplication} from '@loopback/rest'; -import {PingController} from './controllers/ping-controller'; -import {Class, Repository, RepositoryMixin} from '@loopback/repository'; - -export class TodoApplication extends RepositoryMixin(RestApplication) { - constructor(options?: ApplicationConfig) { - super(options); - this.setupControllers(); - } - - setupControllers() { - this.controller(PingController); - } -} -``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md deleted file mode 100644 index 408758a746a7..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/4-todo-model.md +++ /dev/null @@ -1,104 +0,0 @@ -### Building the Todo model - -The Todo model will be the object we use both as a Data Transfer Object (DTO) on -the controller, and as a LoopBack model for the Legacy Juggler implementation. - -Create another folder in `src` called `repositories` and inside of that folder, -create two files: -- `index.ts` -- `todo.repository.ts` - ->**NOTE:** -The `index.ts` file is an export helper file; this pattern is a huge time-saver -as the number of models in your project grows, because it allows you to point -to the _directory_ when attempting to import types from a file within the target -folder. We will use this concept throughout the tutorial! -```ts -// in src/models/index.ts -export * from './foo.model'; -export * from './bar.model'; -export * from './baz.model'; - -// elsewhere... - -// with index.ts -import {Foo, Bar, Baz} from './models'; -// ...and without index.ts -import {Foo} from './models/foo.model'; -import {Bar} from './models/bar.model'; -import {Baz} from './models/baz.model'; -``` - -In our Todo model, we'll create a basic representation of what would go in -a Todo list. Our model will include: -- a unique id -- a title -- a description that details what the todo is all about -- a boolean flag for whether or not we've completed the task. - -For the Legacy Juggler to understand how to work with our model class, it -will need to extend the `Entity` type, as well as provide an override for -the `getId` function, so that it can retrieve a Todo model's ID as needed. - -Additionally, we'll define a `SchemaObject` that represents our Todo model -as an [OpenAPI Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schema-object). -This will give the OpenAPI spec builder the information it needs to describe the -Todo model on your app's OpenAPI endpoints. - -#### src/models/todo.model.ts -```ts -import {Entity, property, model} from '@loopback/repository'; -import {SchemaObject} from '@loopback/openapi-spec'; - -@model() -export class Todo extends Entity { - @property({ - type: 'number', - id: true - }) - id?: number; - - @property({ - type: 'string', - required: true - }) - title: string; - - @property({ - type: 'string' - }) - desc?: string; - - @property({ - type: 'boolean' - }) - isComplete: boolean; - - getId() { - return this.id; - } -} - -export const TodoSchema: SchemaObject = { - title: 'todoItem', - properties: { - id: { - type: 'number', - description: 'ID number of the Todo entry.' - }, - title: { - type: 'string', - description: 'Title of the Todo entry.' - }, - desc: { - type: 'number', - description: 'ID number of the Todo entry.' - }, - isComplete: { - type: 'boolean', - description: 'Whether or not the Todo entry is complete.' - } - }, - required: ['title'], -}; -``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md deleted file mode 100644 index 9cc2ff210e2a..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/5-datasource.md +++ /dev/null @@ -1,34 +0,0 @@ -### Building a Datasource - -Before we can begin constructing controllers and repositories for our -application, we need to define our datasource. - -Create a new folder in the root directory of the project called `config`, -and then inside that folder, create a `datasources.json` file. For now, we'll -be using the memory connector provided with the Juggler. - -#### config/datasources.json -```json -{ - "name": "ds", - "connector": "memory" -} -``` - -Create another folder called `datasources` in the `src` directory, and inside -that folder, create a new file called `db.datasource.ts`. - -#### src/datasources/db.datasource.ts - -```ts -import * as path from 'path'; -import * as fs from 'fs'; -import { DataSourceConstructor, juggler } from '@loopback/repository'; - -const dsConfigPath = path.resolve('config', 'datasources.json'); -const config = require(dsConfigPath); -export const db = new DataSourceConstructor(config); -``` - -This will give us a strongly-typed datasource export that we can work with to -construct our TodoRepository definition. diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md deleted file mode 100644 index b2520590985a..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/6-repository.md +++ /dev/null @@ -1,28 +0,0 @@ -### Create your repository - -Create another folder in `src` called `repositories` and inside of that folder, -create two files: -- `index.ts` (our export helper) -- `todo.repository.ts` - -Our TodoRepository will contain a small base class that uses the -`DefaultCrudRepository` class from `@loopback/repository` and will define the -model type we're working with, as well as its ID type. We'll also inject our -datasource so that this repository can connect to it when executing data -operations. - -#### src/repositories/todo.repository.ts -```ts -import { DefaultCrudRepository, DataSourceType } from '@loopback/repository'; -import { Todo } from '../models'; -import { inject } from '@loopback/core'; - -export class TodoRepository extends DefaultCrudRepository< - Todo, - typeof Todo.prototype.id -> { - constructor(@inject('datasource') protected datasource: DataSourceType) { - super(Todo, datasource); - } -} -``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md deleted file mode 100644 index 484cd906b096..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/7-controller.md +++ /dev/null @@ -1,65 +0,0 @@ -### Create your controller - -Now, we'll create a controller to handle our Todo routes. Create the -`src/controllers` directory and two files inside: -- `index.ts` (export helper) -- `todo.controller.ts` - -In addition to creating the CRUD methods themselves, we'll also be adding -decorators that setup the routing as well as the expected parameters of -incoming requests. - -#### src/controllers/todo.controller.ts -```ts -import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; -import {HttpErrors} from '@loopback/rest'; -import {TodoSchema, Todo} from '../models'; -import {repository} from '@loopback/repository'; -import {TodoRepository} from '../repositories/index'; - -export class TodoController { - constructor( - @repository(TodoRepository.name) protected todoRepo: TodoRepository, - ) {} - @post('/todo') - @param.body('todo', TodoSchema) - async createTodo(todo: Todo) { - if (!todo.title) { - return Promise.reject(new HttpErrors.BadRequest('title is required')); - } - return await this.todoRepo.create(todo); - } - - @get('/todo/{id}') - @param.path.number('id') - @param.query.boolean('items') - async findTodoById(id: number, items?: boolean): Promise { - return await this.todoRepo.findById(id); - } - - @get('/todo') - async findTodos(): Promise { - return await this.todoRepo.find(); - } - - @put('/todo/{id}') - @param.path.number('id') - @param.body('todo', TodoSchema) - async replaceTodo(id: number, todo: Todo): Promise { - return await this.todoRepo.replaceById(id, todo); - } - - @patch('/todo/{id}') - @param.path.number('id') - @param.body('todo', TodoSchema) - async updateTodo(id: number, todo: Todo): Promise { - return await this.todoRepo.updateById(id, todo); - } - - @del('/todo/{id}') - @param.path.number('id') - async deleteTodo(id: number): Promise { - return await this.todoRepo.deleteById(id); - } -} -``` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md b/packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md deleted file mode 100644 index 11bbac64ecfa..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/docs/8-putting-it-together.md +++ /dev/null @@ -1,54 +0,0 @@ -### Putting it all together - -Now that we've got all of our artifacts made, let's set them up in our -application! - -We'll define a new helper function for setting up the repositories, as well -as adding in our new controller binding. - -#### src/application.ts -```ts -import {ApplicationConfig} from '@loopback/core'; -import {RestApplication} from '@loopback/rest'; -import {TodoController, PingController} from './controllers'; -import { - Class, - Repository, - RepositoryMixin, - DataSourceConstructor, -} from '@loopback/repository'; -import {db} from './datasources/db.datasource'; -import {TodoRepository} from './repositories'; - -export class TodoApplication extends RepositoryMixin(RestApplication) { - constructor(options?: ApplicationConfig) { - super(options); - this.setupControllers(); - this.setupRepositories(); - } - - setupControllers() { - this.controller(TodoController); - this.controller(PingController); - } - - setupRepositories() { - // This will allow you to test your application without needing to - // use the "real" datasource! - const datasource = - this.options && this.options.datasource - ? new DataSourceConstructor(this.options.datasource) - : db; - this.bind('datasource').to(datasource); - this.repository(TodoRepository); - } -} -``` - -### Try it out - -Now that your app is ready to go, try it out with your favourite REST client! -Start the app (`npm start`) and then make some REST requests: -- `POST /todo` with a body of `{ "title": "get the milk" }` -- `GET /todo/1` and see if you get your Todo object back. -- `PATCH /todo/1` with a body of `{ "desc": "need milk for cereal" }` diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts deleted file mode 100644 index 13ed083fde3a..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -export * from './dist'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/index.js b/packages/cli/test/sandbox/loopback4-example-getting-started/index.js deleted file mode 100644 index bf96713b9849..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/index.js +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -const application = (module.exports = require('./dist')); - -if (require.main === module) { - // Run the application - application.main(); -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/index.ts deleted file mode 100644 index a6dcc7c6d727..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -// DO NOT EDIT THIS FILE -// Add any aditional (re)exports to src/index.ts instead. -export * from './src'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/package.json b/packages/cli/test/sandbox/loopback4-example-getting-started/package.json deleted file mode 100644 index d8321f150df9..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@loopback/example-getting-started", - "version": "1.0.1-alpha.7", - "description": "An application and tutorial on how to build with LoopBack 4.", - "private": true, - "main": "index.js", - "engines": { - "node": ">=8" - }, - "scripts": { - "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", - "build": "lb-tsc es2017", - "build:apidocs": "lb-apidocs", - "clean": "lb-clean *example-getting-started*.tgz dist package api-docs", - "prepublishOnly": "npm run build && npm run build:apidocs", - "pretest": "npm run build", - "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/acceptance/**/*.js\"", - "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", - "verify": "npm pack && tar xf loopback-getting-started*.tgz && tree package && npm run clean", - "start": "npm run build && node ." - }, - "repository": { - "type": "git", - "url": "https://github.com/strongloop/loopback-next.git" - }, - "license": "MIT", - "dependencies": { - "@loopback/context": "^4.0.0-alpha.32", - "@loopback/core": "^4.0.0-alpha.34", - "@loopback/openapi-spec": "^4.0.0-alpha.25", - "@loopback/openapi-v2": "^4.0.0-alpha.11", - "@loopback/repository": "^4.0.0-alpha.30", - "@loopback/rest": "^4.0.0-alpha.26" - }, - "devDependencies": { - "@loopback/build": "^4.0.0-alpha.13", - "@loopback/testlab": "^4.0.0-alpha.24", - "@types/node": "^8.5.9", - "source-map-support": "^0.5.2", - "typescript": "^2.5.2" - }, - "keywords": [ - "loopback", - "LoopBack", - "example", - "tutorial" - ] -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts deleted file mode 100644 index eadb5bae3f15..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/application.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {ApplicationConfig} from '@loopback/core'; -import {RestApplication} from '@loopback/rest'; -import {TodoController} from './controllers'; -import {TodoRepository} from './repositories'; -import {db} from './datasources/db.datasource'; -/* tslint:disable:no-unused-variable */ -// Class and Repository imports required to infer types in consuming code! -// Do not remove them! -import { - Class, - Repository, - DataSourceConstructor, - RepositoryMixin, -} from '@loopback/repository'; -/* tslint:enable:no-unused-variable */ -export class TodoApplication extends RepositoryMixin(RestApplication) { - constructor(options?: ApplicationConfig) { - super(options); - this.setupRepositories(); - this.setupControllers(); - } - - // Helper functions (just to keep things organized) - setupRepositories() { - // TODO(bajtos) Automate datasource and repo registration via @loopback/boot - // See https://github.com/strongloop/loopback-next/issues/441 - const datasource = - this.options && this.options.datasource - ? new DataSourceConstructor(this.options.datasource) - : db; - // TODO(bajtos) use app.dataSource() from @loopback/repository mixin - // (app.dataSource() is not implemented there yet) - // See https://github.com/strongloop/loopback-next/issues/743 - this.bind('datasource').to(datasource); - this.repository(TodoRepository); - } - - setupControllers() { - // TODO(bajtos) Automate controller registration via @loopback/boot - this.controller(TodoController); - } -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts deleted file mode 100644 index 10f6afeed69b..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -export * from './todo.controller'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts deleted file mode 100644 index 2e58030c68fa..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/controllers/todo.controller.ts +++ /dev/null @@ -1,74 +0,0 @@ -import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; -import {HttpErrors} from '@loopback/rest'; -import {TodoSchema, Todo} from '../models'; -import {repository} from '@loopback/repository'; -import {TodoRepository} from '../repositories/index'; - -export class TodoController { - // TODO(bajtos) Fix documentation (and argument names?) of @repository() - // to allow the usage below. - // See https://github.com/strongloop/loopback-next/issues/744 - constructor( - @repository(TodoRepository.name) protected todoRepo: TodoRepository, - ) {} - @post('/todo') - async createTodo( - @param.body('todo', TodoSchema) - todo: Todo, - ) { - // TODO(bajtos) This should be handled by the framework - // See https://github.com/strongloop/loopback-next/issues/118 - if (!todo.title) { - return Promise.reject(new HttpErrors.BadRequest('title is required')); - } - return await this.todoRepo.create(todo); - } - - @get('/todo/{id}') - async findTodoById( - @param.path.number('id') id: number, - @param.query.boolean('items') items?: boolean, - ): Promise { - return await this.todoRepo.findById(id); - } - - @get('/todo') - async findTodos(): Promise { - return await this.todoRepo.find(); - } - - @put('/todo/{id}') - async replaceTodo( - @param.path.number('id') id: number, - @param.body('todo', TodoSchema) - todo: Todo, - ): Promise { - // REST adapter does not coerce parameter values coming from string sources - // like path & query. As a workaround, we have to cast the value to a number - // ourselves. - // See https://github.com/strongloop/loopback-next/issues/750 - id = +id; - - return await this.todoRepo.replaceById(id, todo); - } - - @patch('/todo/{id}') - async updateTodo( - @param.path.number('id') id: number, - @param.body('todo', TodoSchema) - todo: Todo, - ): Promise { - // REST adapter does not coerce parameter values coming from string sources - // like path & query. As a workaround, we have to cast the value to a number - // ourselves. - // See https://github.com/strongloop/loopback-next/issues/750 - id = +id; - - return await this.todoRepo.updateById(id, todo); - } - - @del('/todo/{id}') - async deleteTodo(@param.path.number('id') id: number): Promise { - return await this.todoRepo.deleteById(id); - } -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts deleted file mode 100644 index 7091892f1172..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/datasources/db.datasource.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import * as path from 'path'; -// The juggler reference must exist for consuming code to correctly infer -// type info used in the "db" export (contained in DataSourceConstructor). -// tslint:disable-next-line:no-unused-variable -import {juggler, DataSourceConstructor} from '@loopback/repository'; - -const dsConfigPath = path.resolve( - __dirname, - '..', - '..', - '..', - 'config', - 'datasources.json', -); -const config = require(dsConfigPath); - -// TODO(bajtos) Ideally, datasources should be created by @loopback/boot -// and registered with the app for dependency injection. -// However, we need to investigate how to access these datasources from -// integration tests where we don't have access to the full app object. -// For example, @loopback/boot can provide a helper function for -// performing a partial boot that creates datasources only. -export const db = new DataSourceConstructor(config); diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts deleted file mode 100644 index 9d23ea75a801..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {TodoApplication} from './application'; -import {RestServer} from '@loopback/rest'; - -export async function main() { - const app = new TodoApplication(); - try { - await app.start(); - } catch (err) { - console.error(`Unable to start application: ${err}`); - } - const server = await app.getServer(RestServer); - console.log(`Server is running on port ${await server.get('rest.port')}`); - return app; -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts deleted file mode 100644 index d7c59a40ea98..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -export * from './todo.model'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts deleted file mode 100644 index 43a092bd481a..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/models/todo.model.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {Entity, property, model} from '@loopback/repository'; -import {SchemaObject} from '@loopback/openapi-spec'; - -@model() -export class Todo extends Entity { - @property({ - type: 'number', - id: true, - }) - id?: number; - - @property({ - type: 'string', - required: true, - }) - title: string; - - @property({ - type: 'string', - }) - desc?: string; - - @property({ - type: 'boolean', - }) - isComplete: boolean; - - getId() { - return this.id; - } -} - -// TODO(bajtos) The schema should be generated from model definition -// See https://github.com/strongloop/loopback-next/issues/700 -// export const TodoSchema = createSchemaFromModel(Todo); -export const TodoSchema: SchemaObject = { - title: 'todoItem', - properties: { - id: { - type: 'number', - description: 'ID number of the Todo entry.', - }, - title: { - type: 'string', - description: 'Title of the Todo entry.', - }, - desc: { - type: 'number', - description: 'ID number of the Todo entry.', - }, - isComplete: { - type: 'boolean', - description: 'Whether or not the Todo entry is complete.', - }, - }, - required: ['title'], -}; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts deleted file mode 100644 index 148a9fba5b74..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -export * from './todo.repository'; diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts deleted file mode 100644 index d8b2f0f8c995..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/src/repositories/todo.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/example-getting-started -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {DefaultCrudRepository, DataSourceType} from '@loopback/repository'; -import {Todo} from '../models'; -import {inject} from '@loopback/core'; - -export class TodoRepository extends DefaultCrudRepository< - Todo, - typeof Todo.prototype.id -> { - constructor(@inject('datasource') protected datasource: DataSourceType) { - super(Todo, datasource); - } -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts deleted file mode 100644 index 9a9d36012dbc..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/test/acceptance/application.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import {createClientForHandler, expect, supertest} from '@loopback/testlab'; -import {RestServer} from '@loopback/rest'; -import {TodoApplication} from '../../src/application'; -import {TodoRepository} from '../../src/repositories/'; -import {givenTodo} from '../helpers'; -import {Todo} from '../../src/models/'; - -describe('Application', () => { - let app: TodoApplication; - let server: RestServer; - let client: supertest.SuperTest; - let todoRepo: TodoRepository; - - before(givenAnApplication); - before(givenARestServer); - before(givenTodoRepository); - before(async () => { - await app.start(); - }); - before(() => { - client = createClientForHandler(server.handleHttp); - }); - after(async () => { - await app.stop(); - }); - - it('creates a todo', async () => { - const todo = givenTodo(); - const response = await client - .post('/todo') - .send(todo) - .expect(200); - expect(response.body).to.containEql(todo); - const result = await todoRepo.findById(response.body.id); - expect(result).to.containEql(todo); - }); - - it('gets a todo by ID', async () => { - const todo = await givenTodoInstance(); - await client - .get(`/todo/${todo.id}`) - .send() - .expect(200, todo); - }); - - it('replaces the todo by ID', async () => { - const todo = await givenTodoInstance(); - const updatedTodo = givenTodo({ - title: 'DO SOMETHING AWESOME', - desc: 'It has to be something ridiculous', - isComplete: true, - }); - await client - .put(`/todo/${todo.id}`) - .send(updatedTodo) - .expect(200); - const result = await todoRepo.findById(todo.id); - expect(result).to.containEql(updatedTodo); - }); - - it('updates the todo by ID ', async () => { - const todo = await givenTodoInstance(); - const updatedTodo = givenTodo({ - title: 'DO SOMETHING AWESOME', - isComplete: true, - }); - await client - .patch(`/todo/${todo.id}`) - .send(updatedTodo) - .expect(200); - const result = await todoRepo.findById(todo.id); - expect(result).to.containEql(updatedTodo); - }); - - it('deletes the todo', async () => { - const todo = await givenTodoInstance(); - await client - .del(`/todo/${todo.id}`) - .send() - .expect(200); - try { - await todoRepo.findById(todo.id); - } catch (err) { - expect(err).to.match(/No Todo found with id/); - return; - } - throw new Error('No error was thrown!'); - }); - - /* - ============================================================================ - TEST HELPERS - These functions help simplify setup of your test fixtures so that your tests - can: - - operate on a "clean" environment each time (a fresh in-memory database) - - avoid polluting the test with large quantities of setup logic to keep - them clear and easy to read - - keep them DRY (who wants to write the same stuff over and over?) - ============================================================================ - */ - function givenAnApplication() { - app = new TodoApplication({ - rest: { - port: 0, - }, - datasource: { - connector: 'memory', - }, - }); - } - - async function givenARestServer() { - server = await app.getServer(RestServer); - } - - async function givenTodoRepository() { - // TODO(bajtos) enhance RepositoryMixin to provide repository getter - // Example usage: - // todoRepo = await app.getRepository(TodoRepository.name) - // See https://github.com/strongloop/loopback-next/issues/745 - todoRepo = (await app.get('repositories.TodoRepository')) as TodoRepository; - } - - async function givenTodoInstance(todo?: Partial) { - return await todoRepo.create(givenTodo(todo)); - } -}); diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts deleted file mode 100644 index a14e478f01f8..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/test/helpers.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {Todo} from '../src/models/index'; - -/* - ============================================================================== - HELPER FUNCTIONS - If you find yourself creating the same helper functions across different - test files, then extracting those functions into helper modules is an easy - way to reduce duplication. - - Other tips: - - - Using the super awesome Partial type in conjunction with Object.assign - means you can: - * customize the object you get back based only on what's important - to you during a particular test - * avoid writing test logic that is brittle with respect to the properties - of your object - - Making the input itself optional means you don't need to do anything special - for tests where the particular details of the input don't matter. - ============================================================================== - * - -/** - * Generate a complete Todo object for use with tests. - * @param todo A partial (or complete) Todo object. - */ -export function givenTodo(todo?: Partial) { - return Object.assign( - new Todo({ - title: 'do a thing', - desc: 'There are some things that need doing', - isComplete: false, - }), - todo, - ); -} diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts b/packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts deleted file mode 100644 index f86329b11b6f..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/test/unit/controllers/todo.controller.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import {expect, sinon} from '@loopback/testlab'; -import {TodoController} from '../../../src/controllers'; -import {TodoRepository} from '../../../src/repositories'; -import {Todo} from '../../../src/models/index'; -import {givenTodo} from '../../helpers'; - -describe('TodoController', () => { - let todoRepo: TodoRepository; - - /* - ============================================================================= - METHOD STUBS - These handles give us a quick way to fake the response of our repository - without needing to wrangle fake repository objects or manage real ones - in our tests themselves. - ============================================================================= - */ - let create: sinon.SinonStub; - let findById: sinon.SinonStub; - let find: sinon.SinonStub; - let replaceById: sinon.SinonStub; - let updateById: sinon.SinonStub; - let deleteById: sinon.SinonStub; - - /* - ============================================================================= - TEST VARIABLES - Combining top-level objects with our resetRepositories method means we don't - need to duplicate several variable assignments (and generation statements) - in all of our test logic. - - NOTE: If you wanted to parallelize your test runs, you should avoid this - pattern since each of these tests is sharing references. - ============================================================================= - */ - let controller: TodoController; - let aTodo: Todo; - let aTodoWithId: Todo; - let aChangedTodo: Todo; - let aTodoList: Todo[]; - - const noError = 'No error was thrown!'; - - beforeEach(resetRepositories); - describe('createTodo', () => { - it('creates a Todo', async () => { - create.resolves(aTodoWithId); - const result = await controller.createTodo(aTodo); - expect(result).to.eql(aTodoWithId); - sinon.assert.calledWith(create, aTodo); - }); - - it('throws if the payload is missing a title', async () => { - const todo = givenTodo(); - delete todo.title; - try { - await controller.createTodo(todo); - } catch (err) { - expect(err).to.match(/title is required/); - sinon.assert.notCalled(create); - return; - } - // Repository stub should not have been called! - throw new Error(noError); - }); - }); - - describe('findTodoById', () => { - it('returns a todo if it exists', async () => { - findById.resolves(aTodoWithId); - expect(await controller.findTodoById(aTodoWithId.id as number)).to.eql( - aTodoWithId, - ); - sinon.assert.calledWith(findById, aTodoWithId.id); - }); - }); - - describe('findTodos', () => { - it('returns multiple todos if they exist', async () => { - find.resolves(aTodoList); - expect(await controller.findTodos()).to.eql(aTodoList); - sinon.assert.called(find); - }); - - it('returns empty list if no todos exist', async () => { - const expected: Todo[] = []; - find.resolves(expected); - expect(await controller.findTodos()).to.eql(expected); - sinon.assert.called(find); - }); - }); - - describe('replaceTodo', () => { - it('successfully replaces existing items', async () => { - replaceById.resolves(true); - expect( - await controller.replaceTodo(aTodoWithId.id as number, aChangedTodo), - ).to.eql(true); - sinon.assert.calledWith(replaceById, aTodoWithId.id, aChangedTodo); - }); - }); - - describe('updateTodo', () => { - it('successfully updates existing items', async () => { - updateById.resolves(true); - expect( - await controller.updateTodo(aTodoWithId.id as number, aChangedTodo), - ).to.eql(true); - sinon.assert.calledWith(updateById, aTodoWithId.id, aChangedTodo); - }); - }); - - describe('deleteTodo', () => { - it('successfully deletes existing items', async () => { - deleteById.resolves(true); - expect(await controller.deleteTodo(aTodoWithId.id as number)).to.eql( - true, - ); - sinon.assert.calledWith(deleteById, aTodoWithId.id); - }); - }); - - function resetRepositories() { - todoRepo = sinon.createStubInstance(TodoRepository); - aTodo = givenTodo(); - aTodoWithId = givenTodo({ - id: 1, - }); - aTodoList = [ - aTodoWithId, - givenTodo({ - id: 2, - title: 'so many things to do', - }), - ] as Todo[]; - aChangedTodo = givenTodo({ - id: aTodoWithId.id, - title: 'Do some important things', - }); - - // Setup CRUD fakes - create = todoRepo.create as sinon.SinonStub; - findById = todoRepo.findById as sinon.SinonStub; - find = todoRepo.find as sinon.SinonStub; - updateById = todoRepo.updateById as sinon.SinonStub; - replaceById = todoRepo.replaceById as sinon.SinonStub; - deleteById = todoRepo.deleteById as sinon.SinonStub; - controller = new TodoController(todoRepo); - } -}); diff --git a/packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json b/packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json deleted file mode 100644 index d9dc5d30c3df..000000000000 --- a/packages/cli/test/sandbox/loopback4-example-getting-started/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/tsconfig", - "extends": "./node_modules/@loopback/build/config/tsconfig.common.json", - "compilerOptions": { - "rootDir": "." - }, - "include": ["index.ts", "src", "test"] -} diff --git a/packages/core/docs.json b/packages/core/docs.json index 01c4befdb150..f4634ab5e5c0 100644 --- a/packages/core/docs.json +++ b/packages/core/docs.json @@ -2,7 +2,6 @@ "content": [ "index.ts", "src/application.ts", - "src/booter.ts", "src/component.ts", "src/index.ts", "src/keys.ts", diff --git a/packages/core/src/application.ts b/packages/core/src/application.ts index ea02d45233b9..9c437d45f57f 100644 --- a/packages/core/src/application.ts +++ b/packages/core/src/application.ts @@ -7,8 +7,6 @@ import {Context, Binding, BindingScope, Constructor} from '@loopback/context'; import {Server} from './server'; import {Component, mountComponent} from './component'; import {CoreBindings} from './keys'; -import {Booter, BootOptions} from './booter'; -import {BootExecutionOptions} from '../index'; /** * Application is the container for various types of artifacts, such as @@ -30,10 +28,10 @@ export class Application extends Context { /** * Register a controller class with this application. * - * @param {Function} controllerCtor The controller class - * (constructor function) + * @param controllerCtor {Function} The controller class + * (constructor function). * @param {string=} name Optional controller name, default to the class name - * @returns {Binding} The newly created binding, you can use the reference to + * @return {Binding} The newly created binding, you can use the reference to * further modify the binding, e.g. lock the value to prevent further * modifications. * @@ -45,84 +43,9 @@ export class Application extends Context { */ controller(controllerCtor: ControllerClass, name?: string): Binding { name = name || controllerCtor.name; - return this.bind(`${CoreBindings.CONTROLLERS_PREFIX}.${name}`) + return this.bind(`controllers.${name}`) .toClass(controllerCtor) - .tag(CoreBindings.CONTROLLERS_TAG); - } - - /** - * Register a booter class / array of classes with this application. - * - * @param {Function | Function[]} booterCls The booter class (constructor function). - * @param {string=} name Optional booter name, defaults to the class name. - * Ignored is cls is an Array and the name defaults to the class name. - * @returns {Binding | Binding[]} The newly created binding(s), you can use the - * reference to further modify the binding, e.g. lock the value to prevent - * further modifications. - * - * ```ts - * class MyBooter implements Booter {} - * app.booter(MyBooter); - * ``` - */ - booter(booterCls: Constructor, name?: string): Binding; - booter(booterCls: Constructor[]): Binding[]; - booter( - booterCls: Constructor | Constructor[], - name?: string, - // tslint:disable-next-line:no-any - ): any { - if (Array.isArray(booterCls)) { - return booterCls.map(cls => this._bindBooter(cls)); - } else { - return this._bindBooter(booterCls, name); - } - } - - /** - * - * @param booterCls A Booter Class - * @param {string} name Name the Booter Class should be bound to - * @returns {Binding} The newly created Binding - */ - private _bindBooter( - booterCls: Constructor, - name?: string, - ): Binding { - name = name || booterCls.name; - return this.bind(`${CoreBindings.BOOTER_PREFIX}.${name}`) - .toClass(booterCls) - .inScope(BindingScope.CONTEXT) - .tag(CoreBindings.BOOTER_TAG); - } - - /** - * Function is responsible for calling all registered Booter classes that - * are bound to the Application instance. Each phase of an instance must - * complete before the next phase is started. - * - * @param {BootExecutionOptions} execOptions Options to control the boot - * process for the Application - */ - async boot(execOptions?: BootExecutionOptions): Promise { - // Get a instance of the BootStrapper - const bootstrapper = await this.get(CoreBindings.BOOTSTRAPPER, { - optional: true, - }); - - // Since bootstrapper is optional, we check to see if instance was returned - if (!bootstrapper) { - console.warn(`No bootstrapper was bound to ${CoreBindings.BOOTSTRAPPER}`); - } else { - // this.options can never be undefined but TypeScript complains so we add - // a check (and throw an error message just to be safe but it should never - // be thrown). - if (this.options) { - await bootstrapper.boot(this.options.bootOptions, execOptions); - } else { - throw new Error(`Application.options need to be defined to use boot`); - } - } + .tag('controller'); } /** @@ -277,10 +200,6 @@ export class Application extends Context { * Configuration for application */ export interface ApplicationConfig { - /** - * Boot Configuration - */ - bootOptions?: BootOptions; /** * Other properties */ diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 351e05e836ce..f1553c2fbcf3 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -6,7 +6,6 @@ import {Constructor, Provider, BoundValue} from '@loopback/context'; import {Server} from './server'; import {Application, ControllerClass} from './application'; -import {Booter} from './booter'; /** * A map of name/class pairs for binding providers @@ -28,10 +27,6 @@ export interface Component { * A map of name/class pairs for binding providers */ providers?: ProviderMap; - /** - * An array of booter classes - */ - booters?: Constructor[]; /** * A map of name/class pairs for servers */ @@ -71,8 +66,4 @@ export function mountComponent(app: Application, component: Component) { app.server(component.servers[serverKey], serverKey); } } - - if (component.booters) { - app.booter(component.booters); - } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e6c5a8089968..cf9d5f64358a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,4 +12,3 @@ export {Server} from './server'; export * from './application'; export * from './component'; export * from './keys'; -export * from './booter'; diff --git a/packages/core/src/keys.ts b/packages/core/src/keys.ts index d280e51dc434..cdf5d9339d93 100644 --- a/packages/core/src/keys.ts +++ b/packages/core/src/keys.ts @@ -23,15 +23,6 @@ export namespace CoreBindings { */ export const SERVERS = 'servers'; - // Binding Constant prefixes / tags - export const CONTROLLERS_PREFIX = 'controllers'; - export const CONTROLLERS_TAG = 'controller'; - - // Key for Binding the BootStrapper Class - export const BOOTSTRAPPER = 'application.bootstrapper'; - export const BOOTER_TAG = 'booter'; - export const BOOTER_PREFIX = 'booters'; - // controller /** * Binding key for the controller class resolved in the current request diff --git a/packages/core/test/unit/application.test.ts b/packages/core/test/unit/application.test.ts index f056817b5e12..5fd3ece0ccc3 100644 --- a/packages/core/test/unit/application.test.ts +++ b/packages/core/test/unit/application.test.ts @@ -4,24 +4,16 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import { - Application, - Server, - Component, - CoreBindings, - Booter, - BootOptions, - BootExecutionOptions, -} from '../..'; -import {Context, Constructor, BindingScope} from '@loopback/context'; +import {Application, Server, Component} from '../../index'; +import {Context, Constructor} from '@loopback/context'; describe('Application', () => { - let app: Application; - beforeEach(givenApp); - describe('controller binding', () => { + let app: Application; class MyController {} + beforeEach(givenApp); + it('binds a controller', () => { const binding = app.controller(MyController); expect(Array.from(binding.tags)).to.containEql('controller'); @@ -35,51 +27,21 @@ describe('Application', () => { expect(binding.key).to.equal('controllers.my-controller'); expect(findKeysByTag(app, 'controller')).to.containEql(binding.key); }); - }); - describe('boot function', () => { - it('calls .boot() if a BootComponent is bound', async () => { - app - .bind(CoreBindings.BOOTSTRAPPER) - .toClass(FakeBootComponent) - .inScope(BindingScope.SINGLETON); - await app.boot(); - const bootComponent = await app.get(CoreBindings.BOOTSTRAPPER); - expect(bootComponent.bootCalled).to.be.True(); - }); - }); - - describe('booter binding', () => { - it('binds a booter', async () => { - const binding = app.booter(TestBooter); - - expect(Array.from(binding.tags)).to.containEql('booter'); - expect(binding.key).to.equal(`${CoreBindings.BOOTER_PREFIX}.TestBooter`); - expect(findKeysByTag(app, CoreBindings.BOOTER_TAG)).to.containEql( - binding.key, - ); - }); - - it('binds an array of booters', async () => { - const bindings = app.booter([TestBooter, TestBooter2]); - const keys = bindings.map(binding => binding.key); - const expected = [ - `${CoreBindings.BOOTER_PREFIX}.TestBooter`, - `${CoreBindings.BOOTER_PREFIX}.TestBooter2`, - ]; - expect(keys.sort()).to.eql(expected.sort()); - expect(findKeysByTag(app, CoreBindings.BOOTER_TAG).sort()).to.eql( - keys.sort(), - ); - }); + function givenApp() { + app = new Application(); + } }); describe('component binding', () => { + let app: Application; class MyController {} class MyComponent implements Component { controllers = [MyController]; } + beforeEach(givenApp); + it('binds a component', () => { app.component(MyComponent); expect(findKeysByTag(app, 'component')).to.containEql( @@ -94,16 +56,14 @@ describe('Application', () => { ); }); - it('binds a booter from a component', () => { - app.component(FakeBooterComponent); - expect(findKeysByTag(app, CoreBindings.BOOTER_TAG)).to.containEql( - `${CoreBindings.BOOTER_PREFIX}.TestBooter`, - ); - }); + function givenApp() { + app = new Application(); + } }); describe('server binding', () => { it('defaults to constructor name', async () => { + const app = new Application(); const binding = app.server(FakeServer); expect(Array.from(binding.tags)).to.containEql('server'); const result = await app.getServer(FakeServer.name); @@ -111,6 +71,7 @@ describe('Application', () => { }); it('allows custom name', async () => { + const app = new Application(); const name = 'customName'; app.server(FakeServer, name); const result = await app.getServer(name); @@ -118,7 +79,7 @@ describe('Application', () => { }); it('allows binding of multiple servers as an array', async () => { - app = new Application(); + const app = new Application(); const bindings = app.servers([FakeServer, AnotherServer]); expect(Array.from(bindings[0].tags)).to.containEql('server'); expect(Array.from(bindings[1].tags)).to.containEql('server'); @@ -131,7 +92,7 @@ describe('Application', () => { describe('start', () => { it('starts all injected servers', async () => { - app = new Application(); + const app = new Application(); app.component(FakeComponent); await app.start(); @@ -142,7 +103,7 @@ describe('Application', () => { }); it('does not attempt to start poorly named bindings', async () => { - app = new Application(); + const app = new Application(); app.component(FakeComponent); // The app.start should not attempt to start this binding. @@ -152,10 +113,6 @@ describe('Application', () => { }); }); - function givenApp() { - app = new Application({projectRoot: __dirname}); - } - function findKeysByTag(ctx: Context, tag: string | RegExp) { return ctx.findByTag(tag).map(binding => binding.key); } @@ -173,14 +130,6 @@ class FakeComponent implements Component { } } -class FakeBootComponent implements Component { - bootCalled = false; - - async boot(options: BootOptions, execOptions?: BootExecutionOptions) { - this.bootCalled = true; - } -} - class FakeServer extends Context implements Server { running: boolean = false; constructor() { @@ -196,15 +145,3 @@ class FakeServer extends Context implements Server { } class AnotherServer extends FakeServer {} - -class TestBooter implements Booter { - async configure() {} -} - -class TestBooter2 implements Booter { - async configure() {} -} - -class FakeBooterComponent implements Component { - booters = [TestBooter]; -} diff --git a/packages/example-getting-started/src/application.ts b/packages/example-getting-started/src/application.ts index fe0f823bac16..d77e18c8d9b8 100644 --- a/packages/example-getting-started/src/application.ts +++ b/packages/example-getting-started/src/application.ts @@ -5,12 +5,14 @@ import {ApplicationConfig} from '@loopback/core'; import {RestApplication} from '@loopback/rest'; -import {BootComponent} from '@loopback/boot'; import {TodoRepository} from './repositories'; import {db} from './datasources/db.datasource'; + /* tslint:disable:no-unused-variable */ +// Do not remove! // Class and Repository imports required to infer types in consuming code! -// Do not remove them! +// Binding and Booter imports are required to infer types for BootMixin! +import {BootMixin, Booter, Binding} from '@loopback/boot'; import { Class, Repository, @@ -18,18 +20,16 @@ import { RepositoryMixin, } from '@loopback/repository'; /* tslint:enable:no-unused-variable */ -export class TodoApplication extends RepositoryMixin(RestApplication) { + +export class TodoApplication extends BootMixin( + RepositoryMixin(RestApplication), +) { constructor(options?: ApplicationConfig) { - options = Object.assign({bootOptions: {projectRoot: __dirname}}, options); super(options); - this.component(BootComponent); + this.projectRoot = __dirname; this.setupRepositories(); } - async boot(): Promise { - await super.boot(); - } - // Helper functions (just to keep things organized) setupRepositories() { // TODO(bajtos) Automate datasource and repo registration via @loopback/boot