Skip to content

Commit

Permalink
feat: add repository booter (#1030)
Browse files Browse the repository at this point in the history
* feat(repository): add AppWithRepository interface

* feat(boot): add repository booter

* feat(example-getting-started): use repository booter
  • Loading branch information
virkt25 authored Feb 28, 2018
1 parent 3fccb5b commit 43ea7a8
Show file tree
Hide file tree
Showing 21 changed files with 339 additions and 55 deletions.
67 changes: 56 additions & 11 deletions packages/boot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ A convention based project Bootstrapper and Booters for LoopBack Applications

A Booter is a Class that can be bound to an Application and is called
to perform a task before the Application is started. A Booter may have multiple
phases to complete its task.
phases to complete its task. The task for a convention based Booter is to discover
and bind Artifacts (Controllers, Repositories, Models, etc.).

An example task of a Booter may be to discover and bind all artifacts of a
given type.
Expand All @@ -28,16 +29,11 @@ $ npm i @loopback/boot

```ts
import {Application} from '@loopback/core';
import {BootMixin} from '@loopback/boot';
import {BootMixin, Booter, Binding} from '@loopback/boot';
class BootApp extends BootMixin(Application) {}

const app = new BootApp();
app.projectRoot = __dirname;
app.bootOptions = {
controlles: {
// Configure ControllerBooter Conventiones here.
}
}

await app.boot();
await app.start();
Expand All @@ -49,14 +45,43 @@ List of Options available on BootOptions Object.
|Option|Type|Description|
|-|-|-|
|`controllers`|`ArtifactOptions`|ControllerBooter convention options|
|`repositories`|`ArtifactOptions`|RepositoryBooter convention options|

### ArtifactOptions

**Add Table for ArtifactOptions**
|Options|Type|Description|
|-|-|-|
|`dirs`|`string \| string[]`|Paths relative to projectRoot to look in for Artifact|
|`extensions`|`string \| string[]`|File extensions to match for Artifact|
|`nested`|`boolean`|Look in nested directories in `dirs` for Artifact|
|`glob`|`string`|A `glob` pattern string. This takes precendence over above 3 options (which are used to make a glob pattern).|

### BootExecOptions

**Add Table for BootExecOptions**
**Experimental support. May be removed or changed in a non-compatible way in future without warning**

To use `BootExecOptions` you must directly call `bootstrapper.boot()` and pass in `BootExecOptions`.
`app.boot()` provided by `BootMixin` does not take any paramters.

```ts
const bootstrapper: Bootstrapper = await this.get(BootBindings.BOOTSTRAPPER_KEY);
const execOptions: BootExecOptions = {
booters: [MyBooter1, MyBooter2],
filter: {
phases: ['configure', 'discover']
}
};

const ctx = bootstrapper.boot(execOptions);
```

You can pass in the `BootExecOptions` object with the following properties:

| Property | Type | Description |
| ---------------- | ----------------------- | ------------------------------------------------ |
| `booters` | `Constructor<Booter>[]` | Array of Booters to bind before running `boot()` |
| `filter.booters` | `string[]` | Names of Booter classes that should be run |
| `filter.phases` | `string[]` | Names of Booter phases to run |

## Available Booters

Expand All @@ -74,11 +99,31 @@ Available Options on the `controllers` object on `BootOptions` are as follows:

|Options|Type|Default|Description|
|-|-|-|-|
|`dirs`|`string | string[]`|`['controllers']`|Paths relative to projectRoot to look in for Controller artifacts|
|`extensions`|`string | string[]`|`['.controller.js']`|File extensions to match for Controller artifacts|
|`dirs`|`string \| string[]`|`['controllers']`|Paths relative to projectRoot to look in for Controller artifacts|
|`extensions`|`string \| string[]`|`['.controller.js']`|File extensions to match for Controller artifacts|
|`nested`|`boolean`|`true`|Look in nested directories in `dirs` for Controller artifacts|
|`glob`|`string`||A `glob` pattern string. This takes precendence over above 3 options (which are used to make a glob pattern).|

### RepositoryBooter

#### Description
Discovers and binds Repository Classes using `app.repository()` (Application must use
`RepositoryMixin` from `@loopback/repository`).

#### Options
The Options for this can be passed via `BootOptions` when calling `app.boot(options:BootOptions)`.

The options for this are passed in a `repositories` object on `BootOptions`.

Available Options on the `repositories` object on `BootOptions` are as follows:

|Options|Type|Default|Description|
|-|-|-|-|
|`dirs`|`string \| string[]`|`['repositories']`|Paths relative to projectRoot to look in for Repository artifacts|
|`extensions`|`string \| string[]`|`['.repository.js']`|File extensions to match for Repository artifacts|
|`nested`|`boolean`|`true`|Look in nested directories in `dirs` for Repository artifacts|
|`glob`|`string`||A `glob` pattern string. This takes precendence over above 3 options (which are used to make a glob pattern).|

## Contributions

- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing#guidelines)
Expand Down
1 change: 1 addition & 0 deletions packages/boot/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"src/booters/base-artifact.booter.ts",
"src/booters/booter-utils.ts",
"src/booters/controller.booter.ts",
"src/booters/repository.booter.ts",
"src/booters/index.ts",
"src/boot.component.ts",
"src/boot.mixin.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/boot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@loopback/context": "^0.1.1",
"@loopback/core": "^0.1.1",
"@loopback/repository": "^0.1.1",
"@types/debug": "0.0.30",
"@types/glob": "^5.0.34",
"debug": "^3.1.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/boot/src/boot.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import {Bootstrapper} from './bootstrapper';
import {Component, Application, CoreBindings} from '@loopback/core';
import {inject, BindingScope} from '@loopback/context';
import {ControllerBooter} from './booters';
import {ControllerBooter, RepositoryBooter} from './booters';
import {BootBindings} from './keys';

/**
Expand All @@ -17,7 +17,7 @@ import {BootBindings} from './keys';
export class BootComponent implements Component {
// Export a list of default booters in the component so they get bound
// automatically when this component is mounted.
booters = [ControllerBooter];
booters = [ControllerBooter, RepositoryBooter];

/**
*
Expand Down
2 changes: 1 addition & 1 deletion packages/boot/src/booters/controller.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class ControllerBooter extends BaseArtifactBooter {
}

/**
* Default ArtifactOptions for a ControllerBooter.
* Default ArtifactOptions for ControllerBooter.
*/
export const ControllerDefaults: ArtifactOptions = {
dirs: ['controllers'],
Expand Down
1 change: 1 addition & 0 deletions packages/boot/src/booters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
export * from './base-artifact.booter';
export * from './booter-utils';
export * from './controller.booter';
export * from './repository.booter';
79 changes: 79 additions & 0 deletions packages/boot/src/booters/repository.booter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {CoreBindings} from '@loopback/core';
import {inject} from '@loopback/context';
import {AppWithRepository} from '@loopback/repository';
import {BaseArtifactBooter} from './base-artifact.booter';
import {BootBindings} from '../keys';
import {ArtifactOptions} from '../interfaces';

/**
* A class that extends BaseArtifactBooter to boot the 'Repository' artifact type.
* Discovered repositories are bound using `app.repository()` which must be added
* to an Application using the `RepositoryMixin` from `@loopback/repository`.
*
* Supported phases: configure, discover, load
*
* @param app Application instance
* @param projectRoot Root of User Project relative to which all paths are resolved
* @param [bootConfig] Repository Artifact Options Object
*/
export class RepositoryBooter extends BaseArtifactBooter {
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE) public app: AppWithRepository,
@inject(BootBindings.PROJECT_ROOT) public projectRoot: string,
@inject(`${BootBindings.BOOT_OPTIONS}#repositories`)
public repositoryOptions: ArtifactOptions = {},
) {
super();

/**
* Repository Booter requires the use of RepositoryMixin (so we have `app.repository`)
* for binding a Repository Class. We check for it's presence and run
* accordingly.
*/
// tslint:disable-next-line:no-any
if (!this.app.repository) {
console.warn(
'app.repository() function is needed for RepositoryBooter. You can add ' +
'it to your Application using RepositoryMixin from @loopback/repository.',
);

/**
* If RepositoryMixin is not used and a `.repository()` function is not
* available, we change the methods to be empty so bootstrapper can
* still run without any side-effects of loading this Booter.
*/
this.configure = async () => {};
this.discover = async () => {};
this.load = async () => {};
} else {
// Set Repository Booter Options if passed in via bootConfig
this.options = Object.assign({}, RepositoryDefaults, repositoryOptions);
}
}

/**
* Uses super method to get a list of Artifact classes. Boot each class by
* binding it to the application using `app.repository(repository);` if present.
*/
async load() {
await super.load();
this.classes.forEach(cls => {
// tslint:disable-next-line:no-any
this.app.repository(cls);
});
}
}

/**
* Default ArtifactOptions for RepositoryBooter.
*/
export const RepositoryDefaults: ArtifactOptions = {
dirs: ['repositories'],
extensions: ['.repository.js'],
nested: true,
};
1 change: 1 addition & 0 deletions packages/boot/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const BOOTER_PHASES = ['configure', 'discover', 'load'];
*/
export type BootOptions = {
controllers?: ArtifactOptions;
repositories?: ArtifactOptions;
/**
* Additional Properties
*/
Expand Down
10 changes: 4 additions & 6 deletions packages/boot/test/acceptance/controller.booter.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import {Client, createClientForHandler, TestSandbox} from '@loopback/testlab';
import {RestServer} from '@loopback/rest';
import {resolve} from 'path';
import {ControllerBooterApp} from '../fixtures/application';
import {BooterApp} from '../fixtures/application';

describe('controller booter acceptance tests', () => {
let app: ControllerBooterApp;
let app: BooterApp;
const SANDBOX_PATH = resolve(__dirname, '../../.sandbox');
const sandbox = new TestSandbox(SANDBOX_PATH);

Expand Down Expand Up @@ -44,10 +44,8 @@ describe('controller booter acceptance tests', () => {
'controllers/multiple.artifact.js.map',
);

const BooterApp = require(resolve(SANDBOX_PATH, 'application.js'))
.ControllerBooterApp;

app = new BooterApp();
const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp;
app = new MyApp();
}

async function stopApp() {
Expand Down
13 changes: 10 additions & 3 deletions packages/boot/test/fixtures/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@

import {RestApplication} from '@loopback/rest';
import {ApplicationConfig} from '@loopback/core';
// tslint:disable:no-unused-variable
import {
RepositoryMixin,
Class,
Repository,
juggler,
} from '@loopback/repository';
// tslint:enable:no-unused-variable

/* tslint:disable:no-unused-variable */
// Binding and Booter imports are required to infer types for BootMixin!
// tslint:disable-next-line:no-unused-variable
import {BootMixin, Booter, Binding} from '../../index';
/* tslint:enable:no-unused-variable */

export class ControllerBooterApp extends BootMixin(RestApplication) {
export class BooterApp extends RepositoryMixin(BootMixin(RestApplication)) {
constructor(options?: ApplicationConfig) {
super(options);
this.projectRoot = __dirname;
Expand Down
4 changes: 2 additions & 2 deletions packages/boot/test/fixtures/multiple.artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

import {get} from '@loopback/openapi-v2';

export class ControllerOne {
export class ArtifactOne {
@get('/one')
one() {
return 'ControllerOne.one()';
}
}

export class ControllerTwo {
export class ArtifactTwo {
@get('/two')
two() {
return 'ControllerTwo.two()';
Expand Down
20 changes: 7 additions & 13 deletions packages/boot/test/integration/controller.booter.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {expect, TestSandbox} from '@loopback/testlab';
import {resolve} from 'path';
import {ControllerBooterApp} from '../fixtures/application';
import {BooterApp} from '../fixtures/application';

describe('controller booter integration tests', () => {
const SANDBOX_PATH = resolve(__dirname, '../../.sandbox');
Expand All @@ -15,15 +15,15 @@ describe('controller booter integration tests', () => {
const CONTROLLERS_PREFIX = 'controllers';
const CONTROLLERS_TAG = 'controller';

let app: ControllerBooterApp;
let app: BooterApp;

beforeEach(resetSandbox);
beforeEach(async () => await sandbox.reset());
beforeEach(getApp);

it('boots controllers when app.boot() is called', async () => {
const expectedBindings = [
`${CONTROLLERS_PREFIX}.ControllerOne`,
`${CONTROLLERS_PREFIX}.ControllerTwo`,
`${CONTROLLERS_PREFIX}.ArtifactOne`,
`${CONTROLLERS_PREFIX}.ArtifactTwo`,
];

await app.boot();
Expand All @@ -46,13 +46,7 @@ describe('controller booter integration tests', () => {
'controllers/multiple.artifact.js.map',
);

const BooterApp = require(resolve(SANDBOX_PATH, 'application.js'))
.ControllerBooterApp;

app = new BooterApp();
}

async function resetSandbox() {
await sandbox.reset();
const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp;
app = new MyApp();
}
});
Loading

0 comments on commit 43ea7a8

Please sign in to comment.