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