diff --git a/docs/Application-generator.md b/docs/Application-generator.md new file mode 100644 index 000000000000..312586feba01 --- /dev/null +++ b/docs/Application-generator.md @@ -0,0 +1,99 @@ +--- +lang: en +title: 'Application generator' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Application-generator.html +summary: +--- + +### Synopsis + +Creates a new LoopBack4 application using REST API. + +``` +lb4 [app] [options] [] +``` + +### Options + +`--applicationName` +: Application name. + +`--description` +: Description of the application. + +`--outDir` +: Project root directory for the application. + +`--tslint` +: Add TSLint to LoopBack4 application project. + +`--prettier` +: Add Prettier to LoopBack4 application project. + +`--mocha` +: Add Mocha to LoopBack4 application project. + +`--loopbackBuild` +: Add @loopback/build module's script set to LoopBack4 application project. + +{% include_relative includes/CLI-std-options.md %} + +### Arguments + +`` - Optional name of the application given as an argument to the command.  +If provided, the tool will use that as the default when prompting for the name. + +### Interactive Prompts + +The tool will prompt you for: + +- Name of the application as will be shown in `package.json`. +If the name had been supplied from the command-line, the prompt is skipped and the application is built with the name from the command-line argument. +Must follow npm naming conventions. + +- Description of the application as will be shown in `package.json`. + +- Name of the directory in which to create your application. +Defaults to the name of the application previously entered. + +- Name of the Application class in `application.ts`. +Defaults to nameApplication. + +- Optional modules to add to the application. These modules are helpful tools to help format, test, and build a LoopBack4 application. +Defaults to `true` for all of the modules. +The prompted modules are: + + - [`tslint`](https://www.npmjs.com/package/tslint) + - [`prettier`](https://www.npmjs.com/package/prettier) + - [`mocha`](https://www.npmjs.com/package/mocha) + - [`@loopback/build`](https://www.npmjs.com/package/@loopback/build) + +### Output + +The core scaffold of a LoopBack4 application generated by the CLI consists of the following files and directories: + +``` +. +├── src/ +| ├── controllers/ +| | └── ping.controller.ts +| ├── datasources/ +| ├── models/ +| ├── repositories/ +| ├── application.ts +| ├── index.ts +| └── sequence.ts +├── test/ +└── package.json +``` + +`ping.controller.ts` is a file used to provide the application with a responsive endpoint. +It contains logic to respond with a greeting message when the appliaction receives a `GET` request from endpoint `/ping`. + +`cd` to the application's newly created directory and run `npm start` to see the application running at `localhost:3000`. +Go to `localhost:3000/ping` to be greeted with a message. + +Once the application has been created, additional generators such as [controller generator](Controller-generator.md) can be run from the application's root directory to further scaffold the application. diff --git a/docs/Application.md b/docs/Application.md new file mode 100644 index 000000000000..cf25de5257e4 --- /dev/null +++ b/docs/Application.md @@ -0,0 +1,217 @@ +--- +lang: en +title: 'Application' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Application.html +summary: +--- + +## What is an Application? + +In LoopBack 4, the [`Application`](http://apidocs.strongloop.com/@loopback%2fcore/#Application) +class is the central class for setting up all of your module's components, +controllers, servers and bindings. The `Application` class extends +[Context](Context.md), and provides the controls for starting and stopping +itself and its associated servers. + +When using LoopBack 4, we strongly encourage you to create your own subclass +of `Application` to better organize your configuration and setup. + +## Making your own application class + +By making your own application class, you can perform several additional +tasks as a part of your setup: +- Pass configuration into the base class constructor +- Perform some asynchronous wireup before application start +- Perform some graceful cleanup on application stop + +{% include code-caption.html content="src/widget-application.ts" %} +```ts +import {Application} from '@loopback/core'; +import {RestComponent, RestServer} from '@loopback/rest'; +import {SamoflangeController, DoohickeyController} from './controllers'; +import {WidgetApi} from './apidef/'; + +export class WidgetApplication extends Application { + constructor() { + // This is where you would pass configuration to the base constructor + // (as well as handle your own!) + super(); + const app = this; // For clarity. + // You can bind to the Application-level context here. + // app.bind('foo').to(bar); + app.component(RestComponent); + app.controller(SamoflangeController); + app.controller(DoohickeyController); + } + + async start() { + // This is where you would asynchronously retrieve servers, providers and + // other components to configure them before launch. + const server = await app.getServer(RestServer); + server.bind('rest.port').to(8080); + server.api(WidgetApi); + // The superclass start method will call start on all servers that are + // bound to the application. + return await super.start(); + } + + async stop() { + // This is where you would do whatever is necessary before stopping your + // app (graceful closing of connections, flushing buffers, etc) + console.log('Widget application is shutting down...') + // The superclass stop method will call stop on all servers that are + // bound to the application. + await super.stop(); + } +} + +``` + +## Configuring your application +Your application can be configured with constructor arguments, bindings, or +a combination of both. + +### Binding configuration +Binding is the most commonly-demonstrated form of application configuration +throughout our examples, and is the recommended method for setting up your +application. + +In addition to the binding functions provided by [Context](Context.md), +the `Application` class also provides some sugar functions for commonly used +bindings, like `component`, `server` and `controller`: + +```ts +export class MyApplication extends Application { + constructor() { + super(); + this.component(MagicSuite); + this.server(RestServer, 'public'); + this.server(RestServer, 'private'); + + this.controller(FooController); + this.controller(BarController); + this.controller(BazController); + } +} +``` + +You can find a complete list of these functions on the +[`Application`](http://apidocs.loopback.io/@loopback%2fcore/#Application) API +docs page. + +Additionally, you can use more advanced forms of binding to fine-tune your +application's configuration: + +```ts +export class MyApplication extends Application { + constructor() { + super(); + this.server(RestServer); + this.controller(FooController); + this.bind('fooCorp.logger').toProvider(LogProvider); + this.bind('repositories.widget') + .toClass(WidgetRepository) + .inScope(BindingScope.SINGLETON); + } +} +``` +In the above example: +- injection calls for `fooCorp.logger` will be handled by the `LogProvider` + class. +- injection calls for `repositories.widget` will be handled by a singleton +instance of the `WidgetRepository` class. + +#### Components +```ts +app.component(MyComponent); +app.component(RestComponent); +``` +The `component` function allows binding of component constructors within +your `Application` instance's context. + +For more information on how to make use of components, +see [Using Components](Using-components.md). + +#### Controllers +```ts +app.controller(FooController); +app.controller(BarController); +``` +Much like the component function, the `controller` function allows +binding of [Controllers](Controllers.md) to the `Application` context. + +#### Servers +```ts +app.server(RestServer); +app.servers([MyServer, GrpcServer]); +``` +The `server` function is much like the previous functions, but +with [Servers](server.md) bulk bindings are possible through the function +`servers`. + +```ts +const app = new Application(); +app.server(RestServer, 'public'); // {'public': RestServer} +app.server(RestServer, 'private'); // {'private': RestServer} +``` +In the above example, the two server instances would be bound to the Application +context under the keys `servers.public`, and `servers.private` respectively. + +### Constructor configuration + +The `Application` class constructor also accepts an +[`ApplicationConfig`](http://apidocs.strongloop.com/@loopback%2fcore/#ApplicationConfig) +object which contains component-level configurations such as +[`RestServerConfig`](http://apidocs.strongloop.com/@loopback%2frest/#RestServerConfig). +It will automatically create bindings for these configurations and later be injected +through dependency injections. Visit [Dependency Injection](Dependency-injection.md) +for more details. + +{% include note.html content=" + Binding configuration such as component binding, provider binding, or binding scopes + are not possible with the constructor-based configuration approach. +" %} + +```ts +export class MyApplication extends RestApplication { + constructor() { + super({ + rest: { + port: 4000, + host: 'my-host' + } + }) + } +} +``` + +## Tips for application setup +Here are some tips to help avoid common pitfalls and mistakes. + +### Use unique bindings +Use binding names that are prefixed with a unique string that does not overlap +with loopback's bindings. As an example, if your application is built for +your employer FooCorp, you can prefix your bindings with `fooCorp`. +```ts +// This is unlikely to conflict with keys used by other component developers +// or within loopback itself! +app.bind('fooCorp.widgetServer.config').to(widgetServerConfig); +``` + +### Avoid use of `getSync` +We provide the [`getSync`](http://apidocs.loopback.io/@loopback%2fcontext/#getSync) +function for scenarios where you cannot asynchronously retrieve your bindings, +such as in constructor bodies. + +However, the number of scenarios in which you must do this are limited, and you +should avoid potential race conditions and retrieve your bindings asynchronously +using the [`get`](http://apidocs.loopback.io/@loopback%2fcontext/#get) function +whenever possible. + +### Use caution with singleton binding scopes +By default, bindings for controllers will instantiate a new instance whenever +they are injected or retrieved from their binding. Your application should only +set singleton binding scopes on controllers when it makes sense to do so. diff --git a/docs/Best-practices-with-Loopback-4.md b/docs/Best-practices-with-Loopback-4.md new file mode 100644 index 000000000000..71c46feaa8f1 --- /dev/null +++ b/docs/Best-practices-with-Loopback-4.md @@ -0,0 +1,19 @@ +--- +lang: en +title: 'Best practices with Loopback 4' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Best-practices-with-Loopback-4.html +summary: +--- + +LoopBack 4 is more than just a framework: It’s an ecosystem that encourages developers to follow best practices through predefined standards. This section will walk through some important guidelines by building an example API for a catalog of products. + +Our best practice follows an "API first" and test-driven development approach: + +1. [**Defining and validating the API**](./Defining-and-validating-the-API.md): This section guides you through constructing your API first before any internal logic is added. +2. [**Testing the API**](./Testing-the-API.md): This section describes the process of writing smoke test for your API and its spec. +3. [**Defining your testing strategy**](./Defining-your-testing-strategy.md): This section discusses the advantages and the process of building a strong testing suite. +4. [**Implementing features**](./Implementing-features.md): This section demonstrates how the tests for each feature of your application should be written, and how to write the logic to make these tests pass. In the example, the tests for the controller, model, repository, data source, and sequence are written and then implemented. +5. [**Preparing the API for consumption**](./Preparing-the-API-for-consumption.md): This section shows how the endpoints can be physically tested using the Swagger UI. diff --git a/docs/Calling-other-APIs-and-Web-Services.md b/docs/Calling-other-APIs-and-Web-Services.md new file mode 100644 index 000000000000..1d3f7a506eb4 --- /dev/null +++ b/docs/Calling-other-APIs-and-Web-Services.md @@ -0,0 +1,17 @@ +--- +lang: en +title: 'Calling other APIs and web services' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Calling-other-APIs-and-web-services.html +summary: +--- +Your API implementation often needs to interact with REST APIs, SOAP Web Services or other forms of APIs. + +How to: +- Set up a Service +- Use services with controllers +- Reuse models between services and controllers + +{% include content/tbd.html %} diff --git a/docs/Command-line-interface.md b/docs/Command-line-interface.md new file mode 100644 index 000000000000..ded9d072f1c2 --- /dev/null +++ b/docs/Command-line-interface.md @@ -0,0 +1,27 @@ +--- +lang: en +title: 'Command-line interface' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Command-line-interface.html +summary: +--- + +LoopBack 4 provides command-line tools to help you get started quickly. The command line tools generate application and extension projects and install their dependencies for you. +The CLI can also help you generate artifacts, such as controllers, for your projects. +Once generated, the scaffold can be expanded with users' own code as needed. + +To use LoopBack 4's CLI, run this command: + +``` +npm install -g @loopback/cli +``` + +## Generating LoopBack projects + +{% include content/lb4-project-commands.html %} + +## Generating LoopBack artifacts + +{% include content/lb4-artifact-commands.html %} diff --git a/docs/Concepts.md b/docs/Concepts.md new file mode 100644 index 000000000000..32deed66172f --- /dev/null +++ b/docs/Concepts.md @@ -0,0 +1,31 @@ +--- +lang: en +title: 'Key concepts' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Concepts.html +summary: +--- + +LoopBack 4 introduces some new concepts that are important to understand: + +- [**Application**](Application.md): In LoopBack 4, the Application class is +the central class for setting up all of your module’s components, controllers, +servers and bindings. The Application class extends [Context](Context.md), and +provides the controls for starting and stopping itself and its associated +servers. +- [**Server**](Server.md): Represents implementation for inbound transports and/or protocols such as REST over http, gRPC over http2, and graphQL over https. It typically listens for requests on a specific port, handle them, and return appropriate responses. +- [**Context**](Context.md): An abstraction of all state and dependencies in your application, that LoopBack uses to “manage” everything. It's a global registry for everything in your app (configurations, state, dependencies, classes, and so on). +- [**Dependency Injection**](Dependency-injection.md): Technique that separates the construction of dependencies of a class or function from its behavior, to keep the code loosely coupled. +- [**Controller**](Controllers.md): Class that implements operations defined by application’s REST API. It implements an application’s business logic and acts as a bridge between the HTTP/REST API and domain/database models. A Controller operates only on processed input and abstractions of backend services / databases. +- [**Route**](Routes.md): Mapping between your API specification and an Operation (JavaScript implementation). It tells LoopBack which function to invoke() given an HTTP request. +- [**Sequence**](Sequence.md): A stateless grouping of [Actions](Sequence.html#actions) that control how a Server responds to requests. +- [**Model**](Model.md): Represents the definition of a model in LoopBack, with respect to the datasource juggler. The `@loopback/repository` module provides special decorators for adding metadata to TypeScript/JavaScript classes to use them with the legacy implementation of Datasource Juggler. In addition, `@loopback/repository-json-schema` module uses the decorators' metadata to build a matching JSON Schema. +- [**Repository**](Repositories.md): Type of Service that represents a collection of data within a DataSource. +- [**Decorator**](Decorators.md): Enables you to annotate or modify your class declarations and members with metadata. +- **Component**: A package that bundles one or more Loopback extensions. + - See [Using components](Using-components.html) and [Creating components](Creating-components.md) for more information. + +{% include note.html title="Review Note" content="_Perhaps this should include some of the material in Thinking in LoopBack_. +" %} diff --git a/docs/Context.md b/docs/Context.md new file mode 100644 index 000000000000..63763b5fb5f5 --- /dev/null +++ b/docs/Context.md @@ -0,0 +1,212 @@ +--- +lang: en +title: 'Context' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Context.html +summary: +--- + +## What is Context? + +- An abstraction of all state and dependencies in your application. +- Context is what LoopBack uses to "manage" everything. +- A global registry for anything/everything in your app (all configs, state, +dependencies, classes, etc). +- An [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) container used to inject dependencies into your code. container used to inject dependencies into your code. + +### Why is it important? + +- You can use the context as a way to give loopback more "info" so that other +dependencies in your app may retrieve it (ie. a centralized place/global +builtin/in-memory storage mechanism). +- LoopBack can help "manage" your resources automatically (through +[Dependency Injection](Dependency-injection.md) and decorators). +- You have full access to updated/real-time application+request state at all +times. + +## How to create a context? + +A context can be created with an optional parent and an optional name. If the +name is not provided, a UUID will be generated as the value. Context instances +can be chained using the `parent` to form a hierarchy. For example, the code +below creates a chain of three contexts: `reqCtx -> serverCtx -> rootCtx`. + +```ts +import {Context} from '@loopback/context'; +const rootCtx = new Context('root-ctx'); // No parent +const serverCtx = new Context(rootCtx, 'server-ctx'); // rootCtx as the parent +const reqCtx = new Context(serverCtx); // No explicit name, a UUID will be generated +``` + +LoopBack's context system allows an unlimited amount of Context instances, +each of which may have a parent Context. However, an application typically +has three "levels" of context: application-level, server-level and request-level. + +## Application-level context (global) + +- stores all the initial and modified app state throughout the entire life of +the app (while the process is alive) +- Generally configured when the application is created (though other things may +modify things in the context while alive/running) + +Here is a simple example: + +```js +const Application = require('@loopback/core').Application; +// Please note `Application` extends from `Context` +const app = new Application(); // `app` is a "Context" +class MyController { ... } +app.controller(MyController); +``` + +In this case, you are using the `.controller` helper method to register a new +controller. The important point to note is `MyController` is actually registered +into the Application Context (`app` is a Context). + +## Server-level context + +Server-level context: +- Is a child of application-level context +- Holds configuration specific to a particular server instance + +Your application will typically contain one or more server instances, each of +which will have the application-level context as its parent. This means that +any bindings that are defined on the application will also be available to the +server(s), unless you replace these bindings on the server instance(s) directly. + +For example, [`@loopback/rest`](https://github.com/strongloop/loopback-next/blob/master/packages/rest) +has the `RestServer` class, which sets up a running HTTP/S server on a port, as +well as defining routes on that server for a REST API. To set the port binding +for the `RestServer`, you would bind the `RestBindings.PORT` key to a number. + +We can selectively re-bind this value for certain server instances to change +what port they use: + +```js +async start() { + // publicApi will use port 443, since it inherits this binding from the app. + app.bind(RestBindings.PORT).to(443); + const publicApi = await app.getServer('public'); + const privateApi = await app.getServer('private'); + // privateApi will be bound to 8080 instead. + privateApi.bind(RestBindings.PORT).to(8080); + await super.start(); +} +``` + +## Request-level context (request) + +Using [`@loopback/rest`](https://github.com/strongloop/loopback-next/blob/master/packages/rest) as an +example, we can create custom sequences that: +- are dynamically created for each incoming server request +- extend the application level context (to give you access to application level dependencies during the request/response lifecycle) +- are garbage collected once the response is sent (memory management) + +Let's see this in action: + +```js +class MySequence extends DefaultSequence { + handle(request, response) { // we provide these value for convenience (taken from the Context) + // but they are still available in the sequence/request context + const req = await this.ctx.get('rest.http.request'); + this.send(`hello ${req.params.name}`); + } +} +``` + +- `this.ctx` is available to your sequence +- allows you to craft your response using resources from the app in addition to +the resources available to the request in real-time (right when you need it) +- `getSync` is one way to get stuff out of the context, there are many others, +see below + +## Storing and retrieving items from a Context + +Items in the Context are indexed via a key and bound to a `ContextValue`. +A `ContextKey` is simply a string value and is used to look up whatever you +store along with the key. For example: + +```js +// app level +const app = new Application(); +app.bind('hello').to('world'); // ContextKey='hello', ContextValue='world' +console.log(app.getSync('hello')); // => 'world' +``` + +In this case, we bind the 'world' string ContextValue to the 'hello' ContextKey. +When we fetch the ContextValue via `getSync`, we give it the ContextKey and it +returns the ContextValue that was initially bound (we can do other fancy things +too -- ie. instantiate your classes, etc) + +The process of registering a ContextValue into the Context is known as +_binding_. Sequence-level bindings work the same way (shown 2 examples before). + +For a list of the available functions you can use for binding, visit +the [Context API Docs](http://apidocs.loopback.io/@loopback%2fcontext). + +## Dependency injection + +- Many configs are adding to the Context during app instantiation/boot time by you/developer. +- When things are registered, the Context provides a way to use your +dependencies during runtime. + +How you access these things is via low level helpers like `app.getSync` or the +`sequence` class that is provided to you as shown in the example in the previous +section. + +However, when using classes, LoopBack provides a better way to get at stuff in +the context via the `@inject` decorator: + +```ts +import {inject} from '@loopback/context'; +import {Application} from '@loopback/core'; +const app = new Application(); +const app.bind('defaultName').to('John'); + +class HelloController { + constructor(@inject('defaultName') name) { + this.name = name; + } + greet(name) { + return `Hello ${name || this.name}`; + } +} +``` + +Notice we just use the default name as though it were available to the +constructor. Context allows LoopBack to give you the necessary information at +runtime even if you do not know the value when writing up the Controller. +The above will print `Hello John` at run time. + +Please refer to [Dependency injection](Dependency-injection.md) for further +details. + +## Context metadata and sugar decorators + +Other interesting decorators can be used to help give LoopBack hints to +additional metadata you may want to provide in order to automatically set things +up. For example, let's take the previous example and make it available on the +`GET /greet` route using decorators provided by +[`@loopback/rest`](https://github.com/strongloop/loopback-next/blob/master/packages/rest): + +```ts +class HelloController { + // tell LoopBack you want this controller method + // to be available at the GET /greet route + @get('/greet') + greet( + // tell LoopBack you want to accept + // the name parameter as a string from + // the query string + @param.query.string('name') + name: string) { + return `Hello ${name}`; + } +} +``` + +These "sugar" decorators allow you to quickly build up your application without +having to code up all the additional logic by simply giving LoopBack hints +(in the form of metadata) to your intent. diff --git a/docs/Controller-generator.md b/docs/Controller-generator.md new file mode 100644 index 000000000000..42dfddf299c5 --- /dev/null +++ b/docs/Controller-generator.md @@ -0,0 +1,135 @@ +--- +lang: en +title: 'Controller generator' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Controller-generator.html +summary: +--- + +{% include content/generator-create-app.html lang=page.lang %} + +### Synopsis + +Adds a new empty controller to a LoopBack application. + +``` +lb4 controller [options] [] +``` + +### Options + +`--controllerType` +: Type of the controller. + +Valid types are `BASIC` and `REST`. +`BASIC` corresponds to an empty controller, whereas `REST` corresponds to +REST controller with CRUD methods. + +{% include_relative includes/CLI-std-options.md %} + +### Arguments + +`` - Optional name of the controller to create as an argument to the command.  +If provided, the tool will use that as the default when it prompts for the name. + +### Interactive Prompts + +The tool will prompt you for: + +- Name of the controller. If the name had been supplied from the command line, +the prompt is skipped and the controller is built with the name from the +command-line argument. +- Type of the controller. You can select from the following types: + * **Empty Controller** - An empty controller definition + * **REST Controller with CRUD Methods** - A controller wired up to a model + and repository definition, with pre-defined CRUD methods. + +#### Empty Controller +If you select the Empty Controller, it will generate a nearly-empty template +based on the given name: + +```ts +// Uncomment these imports to begin using these cool features! + +// import {inject} from '@loopback/context'; + +export class FooController { + constructor() {} +} +``` + +#### REST Controller with CRUD Methods +If you select the REST Controller with CRUD Methods type, you will then be asked +to select: +- The model to use for the CRUD function definitions +- The repository for this model that provides datasource connectivity + +{% include warning.html content= +" +If you do not have a model and repository to select, then you will +receive an error! +" lang=page.lang %} + +Here's an example of what the template will produce given a `Todo` model and +a `TodoRepository`: +```ts +import {Filter, Where} from '@loopback/repository'; +import {post, param, get, put, patch, del} from '@loopback/openapi-v2'; +import {inject} from '@loopback/context'; +import {Todo} from '../models'; +import {TodoRepository} from '../repositories'; + +export class TodoController { + + constructor( + @inject('repositories.TodoRepository') + public todoRepository : TodoRepository, + ) {} + + @post('/todo') + async create(@param.body('obj') obj: Todo) + : Promise { + return await this.todoRepository.create(obj); + } + + @get('/todo/count') + async count(@param.query.string('where') where: Where) : Promise { + return await this.todoRepository.count(where); + } + + @get('/todo') + async find(@param.query.string('filter') filter: Filter) + : Promise { + return await this.todoRepository.find(filter); + } + + @patch('/todo') + async updateAll(@param.query.string('where') where: Where, + @param.body('obj') obj: Todo) : Promise { + return await this.todoRepository.updateAll(where, obj); + } + + @del('/todo') + async deleteAll(@param.query.string('where') where: Where) : Promise { + return await this.todoRepository.deleteAll(where); + } + + @get('/todo/{id}') + async findById(@param.path.number('id') id: number) : Promise { + return await this.todoRepository.findById(id); + } + + @patch('/todo/{id}') + async updateById(@param.path.number('id') id: number, @param.body('obj') + obj: Todo) : Promise { + return await this.todoRepository.updateById(id, obj); + } + + @del('/todo/{id}') + async deleteById(@param.path.number('id') id: number) : Promise { + return await this.todoRepository.deleteById(id); + } +} +``` diff --git a/docs/Controllers.md b/docs/Controllers.md new file mode 100644 index 000000000000..f5afabe2553d --- /dev/null +++ b/docs/Controllers.md @@ -0,0 +1,225 @@ +--- +lang: en +title: 'Controllers' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Controllers.html +summary: +--- + +## Overview + +A `Controller` is a class that implements operations defined by application's REST API. It implements an application's business logic and acts as a bridge between the HTTP/REST API and domain/database models. +A `Controller` operates only on processed input and abstractions of backend services / databases. + +### Review questions + +{% include content/tbd.html %} + +Simplest possible example of a Controller +- app.controller() +- a few methods +- no usage of @api + +How to create a basic `Controller` (beyond the hello world) +- Using DI (@inject) +- Using annotations (eg. @authenticate) +- Defining routes via sugar annoations (@get, @post, @all) +- Errors +- Using `async` / `await` and `Promise`s + +## Operations + +In the previous Operation example, the `greet()` operation was defined as a plain JavaScript function. The example below shows this as a Controller method. + +```js +// plain function Operation +function greet(name) { + return `hello ${name}`; +} + +// Controller method Operation +class MyController { + greet(name) { + return `hello ${name}`; + } +} +``` + +## Routing to Controllers + +This is a basic API Specification used in the following examples. It is an [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/0e51e2a1b2d668f434e44e5818a0cdad1be090b4/versions/2.0.md#operation-object). + +```js +const spec = { + parameters: [{name: 'name', type: 'string', in: 'query'}], + responses: { + '200': { + description: 'greeting text', + schema: {type: 'string'}, + } + } +}; +``` + +There are several ways to define `Routes` to Controller methods. The first example defines a route to the Controller without any magic. + +```js +app.route('get', '/greet', spec, MyController, 'greet'); +``` + +Decorators allow you to annotate your Controller methods with routing metadata, so LoopBack can call the `app.route()` function for you. + +```js +class MyController { + @get('/greet', spec) + greet(name) { + } +} + +app.controller(MyController); +``` + +## Specifying Controller APIs + +For larger LoopBack applications, you can organize your routes into API Specifications using the OpenAPI specification. The `@api` decorator takes a spec with type `ControllerSpec` which comprises of a string `basePath` and a [Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/0e51e2a1b2d668f434e44e5818a0cdad1be090b4/versions/2.0.md#paths-object). It is _not_ a full [Swagger](https://github.com/OAI/OpenAPI-Specification/blob/0e51e2a1b2d668f434e44e5818a0cdad1be090b4/versions/2.0.md#swagger-object) specification. + +```js +app.api({ + basePath: '/', + paths: { + '/greet': { + get: { + 'x-operation-name': 'greet', + 'x-controller-name': 'MyController', + parameters: [{name: 'name', type: 'string', in: 'query'}], + responses: { + '200': { + description: 'greeting text', + schema: {type: 'string'}, + } + } + } + } + } +}); +app.controller(MyController); +``` + +The `@api` decorator allows you to annotate your Controller with a specification, so LoopBack can call the `app.api()` function for you. + +```js +@api({ + basePath: '/', + paths: { + '/greet': { + get: { + 'x-operation-name': 'greet', + 'x-controller-name': 'MyController', + parameters: [{name: 'name', type: 'string', in: 'query'}], + responses: { + '200': { + description: 'greeting text', + schema: {type: 'string'}, + } + } + } + } + } +}) +class MyController { + greet(name) { + } +} +app.controller(MyController); +``` + +## Writing Controller methods + +Below is an example Controller that uses several built in helpers (decorators). These helpers give LoopBack hints about the Controller methods. + +```js +import 'HelloRepostory' from 'path.to.repository'; +import 'HelloMessage' from 'path.to.type'; + +class HelloController { + constructor() { + this.repository = new HelloRepository(); // our repository + } + @get('/messages') + @param.query.number('limit') + async list(limit = 10): Promise { // returns a list of our objects + if (limit > 100) limit = 100; // your logic + return await this.repository.find({limit}); // a CRUD method from our repository + } +} +``` + +- `HelloRepository` extends from `Repository`, which is LoopBack's database abstraction. See [Repositories](./Repositories.md) for more. +- `HelloMessage` is the arbitrary object that `list` returns a list of. +- `@get('/messages')` creates the `Route` for the Operation using `app.route()`. +- `@param.query.number` adds a `number` param with a source of `query`. + +## Handling Errors in Controllers + +In order to specify errors for controller methods to throw, the class `HttpErrors` is used. `HttpErrors` is a class that has been re-exported from [http-errors](https://www.npmjs.com/package/http-errors), and can be found in the `@loopback/rest` package. + +Listed below are some of the most common error codes. The full list of supported codes is found [here](https://github.com/jshttp/http-errors#list-of-all-constructors). + +|Status Code|Error | +|-----------|---------------------| +|400 |BadRequest | +|401 |Unauthorized | +|403 |Forbidden | +|404 |NotFound | +|500 |InternalServerError | +|502 |BadGateway | +|503 |ServiceUnavailable | +|504 |GatewayTimeout | + +The example below shows the previous controller revamped with `HttpErrors` along with a test to verify that the error is thrown properly. + +```js +// the test +import {HelloController} from 'path.to.controller'; +import {HttpErrors, expect} from '@loopback/testlab'; + +describe('Hello Controller', () => { + it('returns 422 Unprocessable Entity for non-positive limit', async () => { + const controller = new HelloController(); + let errCaught: Error; + try { + await controller.list(0.4); // an HttpError should be thrown here + } catch (err) { + errCaught = err; + } + // the test fails here if the error was not thrown + expect(errCaught).to.have.property('statusCode', 422); + expect(errCaught.message).to.match(/non-positive/i); + }); +}); +``` +```js +// the controller +import 'HttpErrors' from '@loopback/rest'; +import 'HelloRepostory' from 'path.to.repository'; +import 'HelloMessage' from 'path.to.type'; + +class HelloController { + repository: HelloRepository; // see Dependency Injection for a better practice + constructor() { + this.repository = new HelloRepository(); + } + @get('/messages') + @param.query.number('limit') + async list(limit = 10): Promise{ + // throw an error when the parameter is not a non-positive integer + if (!Number.isInteger(limit) || limit < 1) + throw new HttpErrors.UnprocessableEntity('limit is non-positive')); + else if (limit > 100) + limit = 100; + return await this.repository.find({limit}); + } +} +``` diff --git a/docs/Crafting-LoopBack-4.md b/docs/Crafting-LoopBack-4.md new file mode 100644 index 000000000000..ac6f777d2e96 --- /dev/null +++ b/docs/Crafting-LoopBack-4.md @@ -0,0 +1,346 @@ +--- +lang: en +title: 'Crafting LoopBack 4' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Crafting-LoopBack-4.html +summary: +--- +## Background + +[LoopBack](http://loopback.io) is an open-source [Node.js](https://nodejs.org) framework built for API developers. Its primary goal is to help create APIs as microservices from existing services/databases and expose them as endpoints for client applications, such as web, mobile, and IoT. LoopBack connects the dots between accepting API requests and interacting with backend resources. By facilitating developers to implement API logic with out of box integration capabilities, LoopBack establishes itself as the API composition layer to [differentiate](http://loopback.io/resources/#compare) from other frameworks, such as [Express](https://expressjs.com), [Hapi](https://hapijs.com), and [Sails](http://sailsjs.com). + +![loopback-composition](/images/lb4/loopback-composition.png) + +Up to version 3.x, LoopBack built on the popular [Express framework](https://expressjs.com). In retrospect, basing LoopBack on Express was the right decision: Standing on the shoulders of Express enabled LoopBack to focus on adding value for API creation experience without reinventing the wheel. LoopBack also has benefitted from the Express ecosystem, especially ready-to-use middleware modules from npm as well as valuable knowledge and support by the community. + +With LoopBack, developers can create and expose APIs just like following a recipe. LoopBack introduces a set of [core concepts](../lb3/LoopBack-core-concepts.md) that represent the key aspects of API implementation. To create APIs out of existing databases or services, developers can simply scaffold a LoopBack application, then add necessary JSON declarations and Node.js code to get their APIs up and running in a few minutes. + +LoopBack uses Express routing and middleware as the plumbing to a request/response pipeline for API use cases, such as authentication, authorization, and routing. Beyond inbound HTTP processing, LoopBack provides integration facilities such as models, datasources, and connectors to allow API logic to interact with various backend systems, including but not limited to, databases, REST APIs, SOAP web services and gRPC microservices. The ability to glue inbound communication and outbound integration makes LoopBack a very powerful framework for API developers. The diagram below illustrates how LoopBack fits into a typical end-to-end API processing flow. + +![loopback-overview](/images/lb4/loopback-overview.png) + +LoopBack has grown significantly in features and users with many years of development and multiple releases. LoopBack has been well-recieved by the developer community. As an indication, the community has developed [many extensions](https://github.com/pasindud/awesome-loopback). The core team has also learned a lot from what we have done as well as great feedback from the community. + +## Why LoopBack 4? + +Like many projects, LoopBack has started to experience growing pains, especially as: + +1. The code base becomes more complicated over time with more modules and more functionality. We would like to have more maintainers and contributors to help out. But the learning curve is getting steep. One of the things to blame is JavaScript itself, which is weakly-typed and lack of constructs such as interfaces to explicitly define contracts between code. There is quite a bit hidden knowledge that is not explicit or obvious for new comers. + +2. Technical debt is accumulating, for example inconsistent designs across modules and feature flags for different behaviors. Here are a few examples: + - Various modules use different registries to manage different types of artifacts, such as remoting metadata, models, datasources, and middleware. + - Different flavors are used to allow custom logic to intercept requests/responses at various layers, such as middleware, remote hooks, CRUD operation hooks, and connector hooks. + - More feature flags were added over time to keep backward compatibility while enabling users to opt-in to new behaviors. + +3. It is becoming more difficult to add new features or fix bugs as some areas start to reach the limit of the current design. + - The `loopback-datasource-juggler` module is a kitchen sink for many things, such as typing, data modeling, validation, aggregation, persistence, and service integration. + - Models are overloaded with multiple responsibilities, such as data representation, persistence, and mapping to REST. Models are tied to datasources and it's not easy to reuse the same model definition against different datasources. + +4. It's not easy to extend the framework without requesting the core team to make code changes in LoopBack modules. The current version of LoopBack has ad-hoc extensibility at various layers. Extension points are not consistently defined. For example, + - Use Express to register middleware. + - Use remoting hooks to intercept remote method invocations. + - Use CRUD hooks to add logic around CRUD operations. + +5. More projects start to use LoopBack as the underlying platform. Such use cases require more knowledge of LoopBack internals and more flexibility and dynamicity to leverage LoopBack to manage and compose artifacts using a metadata driven approach. Some good examples are: + - Multi-tenancy which requires artifact isolation between tenants. + - Metadata APIs to manage/activate model definitions and datasources. + - New interaction patterns for connectors, such as eventing or messaging. + - Extra metadata for model definitions. + +Since the release of 3.x, the team has been brainstorming about how to sustain and advance LoopBack. We did a lot of homework, triaged existing GitHub issues, reached out to community members and downstream products, and evaluated relevant frameworks and technologies to answer to the following questions: + +- Who is the target audience of LoopBack? Why are they interested in LoopBack? What do they use LoopBack for and how do they use it? +- What are the critical pain points? Can we address them incrementally without rebuilding a new foundation? +- What are the most requested features? Is it possible to add such features with the current design? +- What are latest and greatest technologies in our space? What value will they bring in if we start to adopt them? +- How to scale the development and maintenance of LoopBack? How do we allow larger development teams to collaborate on creating APIs using LoopBack? +- How to further grow the community and expand its ecosystem? What can we do to bring more users and contributors to LoopBack? + +LoopBack has gained traction among a spectrum of users beyond Node.js application developers, including: + +- **API developers** - Use LoopBack to create APIs in Node.js. +- **LoopBack maintainers and contributors** - Build and maintain modules by the LoopBack project . +- **Extension developers** - Contribute extensions to LoopBack to augment the framework. +- **Platform developers** - Leverage LoopBack as the base to build their value-added offerings. + +![loopback-ecosystem](/images/lb4/loopback-ecosystem.png) + +The core team decided to make a bold move and rebuild LoopBack to meet the needs of all the above groups. +The decision led to the inception of LoopBack 4, a new generation of API creation platform. +For more information, read the blog post [Announcing LoopBack.next, the Next Step to Make LoopBack Effortlessly Extensible](https://strongloop.com/strongblog/announcing-loopback-next/). + +## Objectives + +LoopBack 4's goals are: + +1. Catch up with latest and greatest technology advances. + - Adopt [ES2016/2017](http://exploringjs.com/es2016-es2017/index.html) and [TypeScript](https://www.typescriptlang.org/) for ease of maintenance and productivity. + - Embrace new standards such as [OpenAPI Spec](https://www.openapis.org/) and [GraphQL](http://graphql.org/). + +2. Promote extensibility to grow the ecosystem. + - Build a minimal core and enable everything else to be implemented via extensions. + - Open the door for more [extension points and extensions](https://github.com/strongloop/loopback-next/issues/512). + +3. Align with cloud native experience for microservices. + - Adopt cloud native microservices by adopting initiatives such as [Cloud Native Computing Foundation](https://www.cncf.io/). + - Make LoopBack a first-class citizen of the microservices ecosystem. + +4. Remove the complexity and inconsistency across modules. + - Use a consistent registry and APIs to manage artifacts and their dependencies. + - Pay down technical debts by refactoring complex modules. + +5. Separate concerns for better composability. + - Introduce new concepts such as controllers and repositories to represent different responsibilities. + - Break down the runtime as a set of services and utilize the extension points/extensions pattern to manage the registration, resolution, and composition. + +## Design principles + +We decided not to take a "big-bang" approach to build LoopBack 4. Instead, we are doing it incrementally in multiple stages with smaller steps. This approach allows us to better engage the community from the beginning. We are following the principles below to pursue architectural simplicity and extensibility: + +1. **Imperative first, declarative later** + + Everything can be done by code via `APIs`. The LoopBack team or community contributors can then create varieties of user experiences with such APIs. For example, with APIs to define models, we allow applications to declare models in JSON or YAML files so that they can be discovered and loaded. An extension can parse other forms of model definition, such as JSON schemas, ES6 classes with decorators, schemas in OpenAPI spec, or even XML schemas into LoopBack model definitions. + + We can also leverage programming constructs such as [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) allow developers to supply metadata in code. Furthermore, LoopBack artifacts can be declared in JSON or YAML files, which will be handy for users to generate and manipulate them by hand or tooling. + +2. **Build minimum features and add more later if necessary** + + Apply YAGNI (You Aint’t Gonna Need It). Design and build for what is needed now, not for what you think you may need in the future. There are many different perspectives in API creation and people ask for a lot of features. Starting with MVP allow us to reach the root of the issues without being derailed by noises and build the absolutely necessary features as the core building blocks. + +3. **Developer experience first** + + Always keep in mind that LoopBack is built for developers by developers. Our first priority is to make API developers' life easier. When we design APIs and user interfaces such as a CLI or GUI, we want to make sure they are intuitive to and natural to their thought process. + +## Implementation stages + +Here are the stages we are marching through toward the final version of LoopBack 4 as illustrated below. + +1. **Rebase and rewrite the core** + + - Leverage TypeScript for better code quality and productivity. + * Provide optional type system for JavaScript. + * Provide planned features from future JavaScript editions to current JavaScript engines. + + - Unify the asynchronous programming model/style. + * 100% promise-based APIs. + * Async/Await as first-class async programming style. + + - Implement an IoC Container for better visibility and extensibility + * Universal registry across different modules + * Dependency injection as a pattern to manage dependencies + + - Introduce Component as packaging model for extensions + * Component can be a npm module or a local directory + * Component encapsulates a list of extensions as a whole + +2. **Validate the core design by implementing an REST/HTTP invocation chain** + + - Add top-down REST API creation which starts with OpenAPI specs. + + - Build sequence of actions for inbound http processing + - Introduce sequence as the composition of actions + - Implement the most critical actions to fulfill the REST API routing and invocation + + - Introduce controllers as entry points for API-related business logic. + + Models are the centerpieces of the current LoopBack applications. . They take multiple responsibilities: + + - Data modeling + - Anchor for API related business logic + - Persistence or service invocation + - Mapping to REST HTTP/JSON endpoints + + - Authentication as a component + + Implement the core functionality of authentication as a component, which includes: + - Decorators to denote authentication requirement + - `authenticate` action to handle authentication + - Extension points for various authentication strategies + +3. **Rebuild our integration and composition capabilities** + + - Introduce repositories to represent data access patterns such as CRUD or Key/Value stores + - Provide a reference implementation of CRUD and KV flavors of repository interfaces using the legacy juggler and connectors + - Refactor/rewrite the juggler into separate modules + - Typing system + - Model and relation definition + - Validation + - Query and mutation language + - DataSource + - Repository interfaces and implementations for data access + - Service interfaces and implementations for service invocations + - Define interfaces and metadata for connectors + - Rewrite connectors + +4. **Declarative metadata and bootstrapping** + + LoopBack manages a set of artifacts, such as models, relations, datasources, connectors, ACLs, controllers, repositories, actions, sequences, components, utility functions, and OpenAPI specs. In addition to the programmatic approach to describe these artifacts by code (apis and decorators), we would like to add declarative support so that they can be declared in JSON/YAML files. + + - Define a new domain-specific language (DSL) in JSON/YAML format and corresponding templates. + - Define the project layout to organize project artifacts. + - Leverage the IoC Context to manage metadata/instances of such artifacts following the extension point/extension pattern. + - Define the lifecycle and serialization/de-serialization requirements for each type of artifact. + - Add a boot component to discover/load/resolve/activate the artifacts. The boot process can be tailored for both tooling and runtime. + +5. **Tooling (CLI & UI)** + + - Add CLI and UI tools to: + - Scaffold LoopBack 4 applications + - Manage artifacts such as sequences, actions, controllers, repositories, services, datasources and models + +6. **Enable cloud native experience** + + - Allow controllers to be exposed as gRPC services + - Allow interaction with other gRPC services + - Integration with microservices deployment infrastructure such as Docker and Kubernetes + - Integration with service mesh + +The following diagram illustrates the high-level building blocks of LoopBack 4: + +![loopback-stack](/images/lb4/loopback-stack.png) + +Please note there is a common layer below the different functional areas in the stack. Let's examine the need to build a new core foundation for LoopBack 4. + +## A new core foundation + +### The core responsibility + +LoopBack itself is already modular. For example, a typical LoopBack 3.x application's dependency graph will have the following npm modules: + +- loopback +- strong-remoting +- loopback-datasource-juggler +- loopback-connector-* +- loopback-component-explorer + +LoopBack manages various artifacts across different modules. The following are a list of built-in types of artifacts that LoopBack 3.x supports out of box: + +- Model definitions/relations: describes data models and their relations +- Validation: validates model instances and properties +- Model configurations: configures models and attaches them to data sources +- Datasources: configures connectivity to backend systems +- Connectors: implements interactions with the underlying backend system +- Components: wraps a module that be bootstrapped with LoopBack +- Remoting: maps JavaScript methods to REST API operations +- ACLs: controls access to protected resources +- Built-in models: provides set of prebuilt models such as User, AccessToken, and Role +- Hooks/interceptors + - Express middleware + - remote hooks + - CRUD operation hooks + - connector hooks +- Security integration + - Identity and token management + - Authentication schemes + - Passport component for various authentication strategies +- Storage component for various local/cloud object storage systems + - Local file system + - Amazon S3 + - Rackspace + - Azure + - Google Cloud + - OpenStack + - IBM Cloud Object Storage +- Push component for mobile push notifications + - iOS + - Android +- Different API styles + - REST + - gRPC + - GraphQL + +Metadata for these artifacts form the knowledge base for LoopBack to glue all the pieces together and build capabilities to handle common API use cases. + +How to represent the metadata and their relations is the key responsibility of the LoopBack core foundation. It needs to provide a consistent way to contribute and consume such building blocks. + +### Key ingredients for the core + +The core foundation for LoopBack 4 is responsible for managing various artifacts independent of the nature of such artifacts. + +- A consistent registry to provide visibility and addressability for all artifacts. + + - Visibility: Each artifact has a unique address and can be accessed via a URI or key. Artifacts can also be visible at different scopes. + + - Extensibility: LoopBack artifacts can be managed by types. New artifact types can be introduced. Instances for a given type can be added, removed, or replaced. Organizing artifacts in a hierarchy of extension points/extensions decouples providers and consumers. + +- Ability to compose with dependency resolution. + + - Composability: It's common that one artifact to have dependencies on other artifacts. With dependency injection or service locator patterns, the core will greatly simplify how multiple artifacts work together. + +- A packaging model for extensions. + + - Pluggability: Extensions can be organized and contributed as a whole. We need to have a packaging model so that extension developers can create their own modules as bundles and plug into a LoopBack application. + +### Why not Express? + +Do we need to build our own core foundation? Can we continue to use Express? Our conclusion is no. Here are the gaps between what Express and LoopBack's needs. + +- **Lack of extensibility**. + + Express is a routing and middleware web framework with minimal functionality of its own: An Express application is essentially a series of middleware function calls. For details, see [Using middleware](http://expressjs.com/en/guide/using-middleware.html). + + Express is only extensibile via middleware. It neither exposes a registry nor provides APIs to manage artifacts such as middleware or routers. For its purpose, Express only deals with middleware-like extensions that intercept http requests/responses. LoopBack needs much more extension points and extensions. + +- **Lack of composability**. + + Express is not composable. For example, `app.use()` is the only way to register a middleware. The order of middleware is determined by the order of `app.use`. This simplistic approach works for a single monolithic application where all middleware are known and arranged ahead of time. But it does not support the case when middleware from other components need to be added between existing ones. LoopBack had to introduce a phase-based extension and hack Express to provide this capability. + + Express doesn't provide any way to manage dependencies between artifact instances either. + +- **Lack of declarative support**. + + In Express, everything is done by JavaScript code as it works exactly as the web site claims: `Fast, unopinionated, minimalist web framework for Node.js`. In contrast, LoopBack is designed to facilitate API creation and composition by conventions and patterns as best practices. More types of constructs are introduced. + +## Deep dive into LoopBack 4 extensibility + +There are several key pillars to make extensibility a reality for LoopBack 4. + +- [Context](Context.md), the IoC container to manage services +- [Dependency injection](Dependency-injection.md) to facilitate composition +- [Decorators](Decorators.md) to supply metadata using annotations +- [Component](Component.md) as the packaging model to bundle extensions + +Please check out [Extending LoopBack 4](Extending-LoopBack-4.md). + +## Rebuilding LoopBack experience on top of the new core + +With the extensible foundation in place, we start to rebuild the LoopBack REST API experience by "eating your own dog food" with the following artifacts: + +- [Sequence and actions](Sequence.md): A sequence of actions to handle HTTP requests/responses. +- [Controllers](Controllers.md): A class with methods to implement API operations behind REST endpoints. +- [Model](Model.md): Definition of data models. +- [Repositories](Repositories.md): Interfaces of access patterns for data sources. + +The features are provided by the following modules: + +- [@loopback/rest](https://github.com/strongloop/loopback-next/tree/master/packages/rest/) +- [@loopback/repository](https://github.com/strongloop/loopback-next/tree/master/packages/repository/) + +## Example for application developers + +Before we go further, let's try to build a 'hello world' application with LoopBack 4. + +### Basic Hello-World + +[@loopback/example-hello-world](https://github.com/strongloop/loopback-next/tree/master/packages/example-hello-world) + +### Intermediate example + +[@loopback/example-log-getting-started](https://github.com/strongloop/loopback-next/tree/master/packages/example-getting-started) + +## Example for extension developers + +### Learn from existing ones + +- [@loopback/example-log-extension](https://github.com/strongloop/loopback-next/tree/master/packages/example-log-extension) +- [@loopback/authentication](https://github.com/strongloop/loopback-next/tree/master/packages/authentication) + +## References + +- https://strongloop.com/strongblog/announcing-loopback-next/ +- https://www.infoq.com/articles/driving-architectural-simplicity +- https://strongloop.com/strongblog/creating-a-multi-tenant-connector-microservice-using-loopback/ +- https://strongloop.com/strongblog/loopback-as-an-event-publisher/ +- https://strongloop.com/strongblog/loopback-as-a-service-using-openwhisk/ diff --git a/docs/Creating-components.md b/docs/Creating-components.md new file mode 100644 index 000000000000..b2fc4fcfe094 --- /dev/null +++ b/docs/Creating-components.md @@ -0,0 +1,411 @@ +--- +lang: en +title: 'Creating components' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Creating-components.html +summary: +--- + + +As explained in [Using Components](Using-components.md), a typical LoopBack component is an npm package exporting a Component class. + +```js +import MyController from './controllers/my-controller'; +import MyValueProvider from './providers/my-value-provider'; + +export class MyComponent { + constructor() { + this.controllers = [MyController]; + this.providers = { + 'my.value': MyValueProvider + }; + } +} +``` + +When a component is mounted to an application, a new instance of the component class is created and then: + - Each Controller class is registered via `app.controller()`, + - Each Provider is bound to its key in `providers` object. + +The example `MyComponent` above will add `MyController` to application's API and create a new binding `my.value` that will be resolved using `MyValueProvider`. + +## Providers + +Providers enable components to export values that can be used by the target application or other components. The `Provider` class provides a `value()` function called by [Context](Context.md) when another entity requests a value to be injected. + +```js +import {Provider} from '@loopback/context'; + +export class MyValueProvider { + value() { + return 'Hello world'; + } +} +``` + +### Specifying binding key + +Notice that the provider class itself does not specify any binding key, the key is assigned by the component class. + +```js +import MyValueProvider from './providers/my-value-provider'; + +export class MyComponent { + constructor() { + this.providers = { + 'my-component.my-value': MyValueProvider + }; + } +} +``` + +### Accessing values from Providers + +Applications can use `@inject` decorators to access the value of an exported Provider. +If you’re not familiar with decorators in TypeScript, see [Key Concepts: Decorators](Decorators.md) + +```js +const app = new Application(); +app.component(MyComponent); + +class MyController { + constructor(@inject('my-component.my-value') greeting) { + // LoopBack sets greeting to 'Hello World' when creating a controller instance + this.greeting = greeting; + } + + @get('/greet') + greet() { + return this.greeting; + } +} +``` + +{% include note.html title="A note on binding names" content="To avoid name conflicts, add a unique prefix to your binding key (for example, `my-component.` in the example above). See [Reserved binding keys](Reserved-binding-keys.md) for the list of keys reserved for the framework use. +" %} + +### Asynchronous providers + +Provider's `value()` method can be asynchronous too: + +```js +const request = require('request-promise-native'); +const weatherUrl = + 'http://samples.openweathermap.org/data/2.5/weather?appid=b1b15e88fa797225412429c1c50c122a1' + +export class CurrentTemperatureProvider { + async value() { + const data = await(request(`${weatherUrl}&q=Prague,CZ`, {json:true}); + return data.main.temp; + } +} +``` + +In this case, LoopBack will wait until the promise returned by `value()` is resolved, and use the resolved value for dependency injection. + +### Working with HTTP request/response + +In some cases, the Provider may depend on other parts of LoopBack; for example the current `request` object. The Provider's constructor should list such dependencies annotated with `@inject` keyword, so that LoopBack runtime can resolve them automatically. + +```js +const uuid = require('uuid/v4'); + +class CorrelationIdProvider { + constructor(@inject('http.request') request) { + this.request = request; + } + + value() { + return this.request.headers['X-Correlation-Id'] || uuid(); + } +} +``` + +## Modifying request handling logic + +A frequent use case for components is to modify the way requests are handled. For example, the authentication component needs to verify user credentials before the actual handler can be invoked; or a logger component needs to record start time and write a log entry when the request has been handled. + +The idiomatic solution has two parts: + + 1. The component should define and bind a new [Sequence action](Sequence.html#actions), for example `authentication.actions.authenticate`: + + ```js + class AuthenticationComponent { + constructor() { + this.providers = { + 'authentication.actions.authenticate': AuthenticateActionProvider + }; + } + } + ``` + + A sequence action is typically implemented as an `action()` method in the provider. + + ```js + class AuthenticateActionProvider { + // Provider interface + value() { + return request => this.action(request); + } + + // The sequence action + action(request) { + // authenticate the user + } + } + ``` + + It may be tempting to put action implementation directly inside the anonymous arrow function returned by provider's `value()` method. We consider that as a bad practice though, because when an error occurs, the stack trace will contain only an anonymous function that makes it more difficult to link the entry with the sequence action. + + + 2. The application should use a custom `Sequence` class which calls this new sequence action in an appropriate place. + + ```js + class AppSequence implements SequenceHandler { + constructor( + @inject('sequence.actions.findRoute') findRoute, + @inject('sequence.actions.invokeMethod') invoke, + @inject('sequence.actions.send') send: Send, + @inject('sequence.actions.reject') reject: Reject, + // Inject the new action here: + @inject('authentication.actions.authenticate') authenticate + ) { + this.findRoute = findRoute; + this.invoke = invoke; + this.send = send; + this.reject = reject; + this.authenticate = authenticate; + } + + async handle(req: ParsedRequest, res: ServerResponse) { + try { + const route = this.findRoute(req); + + // Invoke the new action: + const user = await this.authenticate(req); + + const args = await parseOperationArgs(req, route); + const result = await this.invoke(route, args); + this.send(res, result); + } catch (err) { + this.reject(res, req, err); + } + } + } + ``` + +### Accessing Elements contributed by other Sequence Actions + +When writing a custom sequence action, you need to access Elements contributed by other actions run in the sequence. For example, `authenticate()` action needs information about the invoked route to decide whether and how to authenticate the request. + +Because all Actions are resolved before the Sequence `handle` function is run, Elements contributed by Actions are not available for injection yet. To solve this problem, use `@inject.getter` decorator to obtain a getter function instead of the actual value. This allows you to defer resolution of your dependency only until the sequence action contributing this value has already finished. + +```js +export class AuthenticationProvider { + constructor( + @inject.getter(BindingKeys.Authentication.STRATEGY) + readonly getStrategy + ) { + this.getStrategy = getStrategy; + } + + value() { + return request => this.action(request); + } + + async action(request) { + const strategy = await getStrategy(); + // ... + } +} +``` + + +### Contributing Elements from Sequence Actions + +Use `@inject.setter` decorator to obtain a setter function that can be used to contribute new Elements to the request context. + +```js +export class AuthenticationProvider { + constructor( + @inject.getter(BindingKeys.Authentication.STRATEGY) + readonly getStrategy, + @inject.setter(BindingKeys.Authentication.CURRENT_USER) + readonly setCurrentUser, + ) { + this.getStrategy = getStrategy; + this.setCurrentUser = setCurrentUser; + } + + value() { + return request => this.action(request); + } + + async action(request) { + const strategy = await getStrategy(); + const user = // ... authenticate + setCurrentUser(user); + return user; + } +} +``` + +## Extends Application with Mixin + +When binding a component to an app, you may want to extend the app with the component's +properties and methods by using mixins. + +For an overview of mixins, see [Mixin](Mixin.md). + +An example of how a mixin leverages a component is `RepositoryMixin`. +Suppose an app has multiple components with repositories bound to each of them. +You can use function `RepositoryMixin()` to mount those repositories to application level context. + +The following snippet is an abbreviated function +[`RepositoryMixin`](https://github.com/strongloop/loopback-next/blob/master/packages/repository/src/repository-mixin.ts): + +{% include code-caption.html content="mixins/src/repository-mixin.ts" %} +```js +export function RepositoryMixin>(superClass: T) { + return class extends superClass { + constructor(...args: any[]) { + super(...args); + } + } + + /** + * Add a component to this application. Also mounts + * all the components' repositories. + */ + public component(component: Class) { + super.component(component); + this.mountComponentRepository(component); + } + + mountComponentRepository(component: Class) { + const componentKey = `components.${component.name}`; + const compInstance = this.getSync(componentKey); + + // register a component's repositories in the app + if (compInstance.repositories) { + for (const repo of compInstance.repositories) { + this.repository(repo); + } + } + } +} +``` + +Then you can extend the app with repositories in a component: + +{% include code-caption.html content="index.ts" %} + +```js +import {RepositoryMixin} from 'mixins/src/repository-mixin'; +import {Application} from '@loopback/core'; +import {FooComponent} from 'components/src/Foo'; + +class AppWithRepoMixin extends RepositoryMixin(Application) {}; +let app = new AppWithRepoMixin(); +app.component(FooComponent); + +// `app.find` returns all repositories in FooComponent +app.find('repositories.*'); +``` + +## Configuring components + +More often than not, the component may want to offer different value providers depending on the configuration. For example, a component providing an email API may offer different transports (stub, SMTP, and so on). + +Components should use constructor-level [Dependency Injection](Context.html#dependency-injection) to receive the configuration from the application. + +```js +class EmailComponent { + constructor(@inject('config#components.email') config) { + this.config = config; + this.providers = { + 'sendEmail': config.transport == 'stub' ? + StubTransportProvider : + SmtpTransportProvider, + }; + } +} +``` + +## Creating your own servers + +LoopBack 4 has the concept of a Server, which you can use to create your own +implementations of REST, SOAP, gRPC, MQTT and more. For an overview, see +[Server](Server.md). + +Typically, you'll want server instances that listen for traffic on one or more +ports (this is why they're called "servers", after all). This leads into a key +concept to leverage for creating your custom servers. + +### Controllers and routing +LoopBack 4 developers are strongly encouraged to use controllers for their +modules, and this naturally leads to the concept of routing. + +No matter what protocol you intend to use for your custom server, you'll need +to use some algorithm to determine _which_ controller and function to send +request data to, and that means you need a router. + +For example, consider a "toy protocol" similar to the JSON RPC +specification (but nowhere near as complete or robust). + +The toy protocol will require a JSON payload with three properties: `controller`, +`method`, and `input`. + +An example request would look something like this: +```json +{ + "controller": "GreetController", + "method": "basicHello", + "input": { + "name": "world", + } +} +``` + +You can find the code for our sample RPC server implementation +[over here](https://github.com/strongloop/loopback4-example-rpc-server). + +### Trying it out +First, install your dependencies and then start the application: +``` +npm i && npm start +``` + +Now, try it out: start the server and run a few REST requests. Feel free to use +whatever REST client you'd prefer (this example will use `curl`). +```sh +# Basic Greeting Calls +$ curl -X POST -d '{ "controller": "GreetController", "method": "basicHello" }' -H "Content-Type: application/json" http://localhost:3000/ +Hello, World! +$ curl -X POST -d '{ "controller": "GreetController", "method": "basicHello", "input": { "name": "Nadine" } }' -H "Content-Type: application/json" http://localhost:3000/ +Hello, Nadine! +# Advanced Greeting Calls +$ curl -X POST -d '{ "controller": "GreetController", "method": "hobbyHello", "input": { "name": "Nadine" } }' -H "Content-Type: application/json" http://localhost:3000/ +Hello, Nadine! I heard you like underwater basket weaving. +$ curl -X POST -d '{ "controller": "GreetController", "method": "hobbyHello", "input": { "name": "Nadine", "hobby": "extreme mountain biking" } }' -H "Content-Type: application/json" http://localhost:3000/ +Hello, Nadine! I heard you like extreme mountain biking. +``` + +While a typical protocol server would be a lot more involved in the +implementation of both its router and server, the general concept remains +the same, and you can use these tools to make whatever server you'd like. + +### Other considerations +Some additional concepts to add to your server could include: +- Pre-processing of requests (changing content types, checking the request body, +etc) +- Post-processing of responses (removing sensitive/useless information) +- Caching +- Logging +- Automatic creation of default endpoints +- and more... + +LoopBack 4's modularity allows for custom servers of all kinds, while still +providing key utilities like context and injection to make your work easier. diff --git a/docs/Creating-decorators.md b/docs/Creating-decorators.md new file mode 100644 index 000000000000..cd19c2c42a39 --- /dev/null +++ b/docs/Creating-decorators.md @@ -0,0 +1,12 @@ +--- +lang: en +title: 'Creating decorators' +keywords: LoopBack 4.0, LoopBack 4 +layout: readme +source: loopback-next +file: packages/metadata/README.md +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Creating-decorators.html +summary: A tutorial to use `@loopback/metadata` module to create new decorators +--- diff --git a/docs/DEVELOPING.md b/docs/DEVELOPING.md index f27e10965872..382084fb2e82 100644 --- a/docs/DEVELOPING.md +++ b/docs/DEVELOPING.md @@ -243,7 +243,7 @@ $ npm publish --access=public Please register the new package in the following files: - Update [MONOREPO.md](../MONOREPO.md) - insert a new table row to describe the new package, please keep the rows sorted by package name. - - Update [docs/apidocs.html](./docs/apidocs.html) - add a link to API docs for this new package. + - Update [docs/apidocs.html](./apidocs.html) - add a link to API docs for this new package. - Update [CODEOWNERS](./CODEOWNERS) - add a new entry listing the primary maintainers (owners) of the new package - Ask somebody from the IBM team (e.g. [@bajtos](https://github.com/bajtos), [@raymondfeng](https://github.com/raymondfeng) or [@kjdelisle](https://github.com/kjdelisle)) to enlist the new package on http://apidocs.loopback.io/ diff --git a/docs/Decorators.md b/docs/Decorators.md new file mode 100644 index 000000000000..47d32032b564 --- /dev/null +++ b/docs/Decorators.md @@ -0,0 +1,471 @@ +--- +lang: en +title: 'Decorators' +keywords: LoopBack 4.0, LoopBack-Next +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Decorators.html +summary: +--- + +A decorator allows you to annotate or modify your class declarations and members with metadata. + +## Introduction + +*If you're new to Decorators in TypeScript, see [here](https://www.typescriptlang.org/docs/handbook/decorators.html) for more info.* + +Decorators give LoopBack the flexibility to modify your plain TypeScript classes +and properties in a way that allows the framework to better understand how to +make use of them, without the need to inherit base classes or add functions +that tie into an API. + +As a default, LoopBack comes with some pre-defined decorators: + +- [Route Decorators](#route-decorators) +- [Dependency Injection](#dependency-injection) +- [Authentication Decorator](#authentication-decorators) +- [Repository Decorators](#repository-decorators) + +## Route Decorators + +Route decorators are used to expose controller methods as REST API operations. +If you are not familiar with the concept Route or Controller, please see [LoopBack Route](routes.md) +and [LoopBack Controller](controllers.md) to learn more about them. + +By calling a route decorator, you provide OpenAPI specification to describe the +endpoint which the decorated method maps to. You can choose different decorators +accordingly or do a composition of them: + +### API Decorator + + Syntax: [`@api(spec: ControllerSpec)`](http://apidocs.loopback.io/@loopback%2fcore/#783) + + `@api` is a decorator for controller constructor, it's called before a controller + class. `@api` is used when you have a base path and a Paths Object, which + contains all path definitions of your controller. Please note the api specs defined + with `@api` will override other api specs defined inside the controller. For example: + + ```ts + @api({ + basePath: '/', + paths: { + '/greet': { + get: { + 'x-operation-name': 'greet', + 'x-controller-name': 'MyController', + parameters: [{name: 'name', type: 'string', in: 'query'}], + responses: { + '200': { + description: 'greeting text', + schema: {type: 'string'}, + } + } + } + } + } + }) + class MyController { + // The operation endpoint defined here will be overriden! + @get('/foo') + @param.query.number('limit') + greet(name) { + } + } + app.controller(MyController); + ``` + + A more detailed explanation can be found in [Specifying Controller APIs](controllers.htm#specifying-controller-apis) + +### Operation Decorator + + Syntax: [`@operation(verb: string, path: string, spec?: OperationObject)`](http://apidocs.loopback.io/@loopback%2fcore/#818) + + `@operation` is a controller method decorator. It exposes a Controller method as + a REST API operation. You can specify the verb, path, parameters and response + as specification of your endpoint, for example: + + ```ts + const spec = { + parameters: [{name: 'name', type: 'string', in: 'query'}], + responses: { + '200': { + description: 'greeting text', + schema: {type: 'boolean'}, + } + } + }; + class MyController { + @operation('HEAD', '/checkExist', spec) + checkExist(name) { + } + } + ``` + +### Commonly-used Operation Decorators + + Syntax: [`@get(path: string, spec?: OperationObject)`](http://apidocs.loopback.io/@loopback%2fcore/#798) + + Same Syntax for decorators [`@post`](http://apidocs.loopback.io/@loopback%2fcore/#802) + , [`@put`](http://apidocs.loopback.io/@loopback%2fcore/#806) + , [`@patch`](http://apidocs.loopback.io/@loopback%2fcore/#810) + , [`@del`](http://apidocs.loopback.io/@loopback%2fcore/#814) + + You can call these sugar operation decorators as a shortcut of `@operation`, for example: + ```ts + class MyController { + @get('/greet', spec) + greet(name) { + } + } + ``` + + is equivalent to + + ```ts + class MyController { + @operation('GET', '/greet', spec) + greet(name) { + } + } + ``` + + For more usage, refer to [Routing to Controllers](controllers.htm#routing-to-controllers) + +### Parameter Decorator + + Syntax: [`@param(paramSpec: ParameterObject)`]() + + Optional Pattern: `@param.${in}.${type}(${name})` + + `@param` can be applied to method itself or specific parameters, it describes + an input parameter of a Controller method. + + For example: + + ```ts + // example 1: decorator `@param ` applied on method level + class MyController { + @get('/') + @param(offsetSpec) + @param(pageSizeSpec) + list(offset?: number, pageSize?: number) {} + } + ``` + + or + + ```ts + // example 2: decorator `@param` applied on parameter level + class MyController { + @get('/') + list( + @param(offsetSpec) offset?: number, + @param(pageSizeSpec) pageSize?: number, + ) {} + } + ``` + + In the first example, we apply multiple decorators to a single declaration. + The order in which the decorators are called is important because a `@param` + decorator must be applied after an operation decorator. + To learn more about TypeScript decorator composition, + refer to [TypeScript Decorator Documentation](https://www.typescriptlang.org/docs/handbook/decorators.html) + + Please note method level `@param` and parameter level `@param` are mutually exclusive, + you can not mix and apply them to the same parameter. + + You can also use this pattern to make it quicker to define params: `@param.${in}.${type}(${name})` + + - in: one of the following values: `query`, `header`, `path`, `formData`, `body` + - type: one of the following values: `string`, `number`, `boolean`, `integer` + - name: a `string`, name of the parameter + + So an example would be `@param.query.number('offset')`. + You can find the specific usage in [Writing Controller methods](controller.htm#writing-controller-methods) + +## Dependency Injection + +`@inject` is a decorator to annotate method arguments for automatic injection by +LoopBack's IoC container. + +The injected values are applied to a constructed instance, so it can only be used on +non-static properties or constructor parameters of a Class. + +The `@inject` decorator allows you to inject dependencies bound to any implementation +of the [Context](#context) object, such as an Application instance or a request context instance. +You can bind values, class definitions and provider functions to those contexts and +then resolve values (or the results of functions that return those values!) in other +areas of your code. + +```ts +// application.ts +import {Application} from '@loopback/core'; +import 'fs-extra'; +class MyApp extends Application { + constructor() { + super(); + const app = this; + const widgetConf = JSON.parse(fs.readSync('./widget-config.json')); + function logInfo(info) { + console.log(info); + } + app.bind('config.widget').to(widgetConf); + app.bind('logger.widget').to(logInfo); + } +} +``` + +Now that we've bound the 'config.widget' key to our configuration object, +and 'logger.widget' key to the function `logInfo()`, +we can inject them in our WidgetController: + +```ts +// widget-controller.ts +import {widgetSpec} from '../apispec'; +@api(widgetSpec) +class WidgetController { + // injection for property + @inject('logger.widget') + private logger: Function + + // injection for constructor parameter + constructor( + @inject('config.widget') protected widget: any + // This will be resolved at runtime! + ) {} + // etc... +} +``` + +A few variants of `@inject` are provided to declare special forms of dependencies: + +- `@inject.getter`: inject a getter function that returns a promise of the bound value of the key + +Syntax: `@inject.getter(bindingKey: string)`. + +```ts +class HelloController { + constructor( + @inject.getter('authentication.currentUser') + private userGetter: Getter) {} + + async greet() { + const user = await this.userGetter(); + return `Hello, ${user.name}`; + } +} +``` + +- `@inject.setter`: inject a setter function to set bound value of the key + +Syntax: `@inject.setter(bindingKey: string)`. + +```ts +class HelloController { + constructor( + @inject.setter('greeting') + private greetingSetter: Setter) {} + + greet() { + greetingSetter('my-greeting-message'); + } +} +``` + +- `@inject.tag`: inject an array of values by a pattern or regexp to match bindng tags + +Syntax: `@inject.tag(tag: string | RegExp)`. + +```ts + class Store { + constructor(@inject.tag('store:location') public locations: string[]) {} + } + + ctx.bind('store').toClass(Store); + ctx + .bind('store.locations.sf') + .to('San Francisco') + .tag('store:location'); + ctx + .bind('store.locations.sj') + .to('San Jose') + .tag('store:location'); + const store: Store = ctx.getSync('store'); + // `store.locations` is now `['San Francisco', 'San Jose']` +``` + +- `@inject.context`: inject the current context + +Syntax: `@inject.context()`. + +```ts + class MyComponent { + constructor(@inject.context() public ctx: Context) {} + } + + const ctx = new Context(); + ctx.bind('my-component').toClass(MyComponent); + const component: MyComponent = ctx.getSync('my-component'); + // `component.ctx` should be the same as `ctx` +``` + +**NOTE**: It's recommended to use `@inject` with specific keys for dependency injection if possible. Use `@inject.context` only when the code need to access the current context object for advanced use cases. + +For more information, see the [Dependency Injection](Dependency-Injection.htm) section under [LoopBack Core Concepts](Concepts.md) + +## Authentication Decorator + + Syntax: `@authenticate(strategyName: string, options?: Object)` + + Marks a controller method as needing an authenticated user. + This decorator requires a strategy name as a parameter. + + Here's an example using 'BasicStrategy': to authenticate user in function `whoAmI`: + + ```ts + // my-controller.ts + import { authenticate } from '@loopback/authentication'; + import { inject } from '@loopback/context'; + + class MyController { + constructor( + @inject(BindingKeys.Authentication.CURRENT_USER) + private user: UserProfile, + ) {} + + @authenticate('BasicStrategy') + async whoAmI() : Promise { + return this.user.id; + } + } + ``` + +## Repository Decorators + + As a Domain-driven design concept, + the repository is a layer between your domain object and data mapping layers + using a collection-like interface for accessing domain objects. + + In LoopBack, a domain object is usually a TypeScript/JavaScript Class instance, + and a typical example of a data mappting layer module could be a database's node.js driver. + + LoopBack repository encapsulates your TypeScript/JavaScript Class instance, + and its methods that communicate with your database. + It is an interface to implement data persistence. + + Repository decorators are used for defining models(domain objects) for use with your chosen datasources, + and the navigation strategies among models. + + If you are not familiar with repository related concepts like `Model`, `Entity` and `Datasource`, + please see LoopBack concept [Repositories](#Repositories.md) to learn more. + +### Model Decorators + + Model is a Class that LoopBack builds for you to organize the data that + share same configurations and properties. + You can use model decorators to define a model and its properties. + +#### Model Decorator + + Syntax: `@model(definition?: ModelDefinitionSyntax)` + + Model decorator is a Class decorator. + In LoopBack 4, we inherit the model definition format from LoopBack 3, + you can find it in [Model definition JSON file](../lb3/Model-definition-JSON-file.md). + For usage examples, see [Define Models](Repositories.html#define-models) + + *Please note we will elaborate more about model and model definition in #Model.htm,* + *and replace the link above with LoopBack 4 link* + + By using a model decorator, you can define a model as your repository's metadata, + then you have two choices to create the repository instance: + + One is to inject your repository and resolve it with Legacy Juggler that complete + with CRUD operations for accessing the model's data. + A use case can be found in section [Repository decorator](#repository-decorator) + + The other one is defining your own repository without using legacy juggler, + and use an ORM/ODM of your choice. + + ```ts + // Missing example here + // Will be provided in Model.htm + // refer to [example code](https://github.com/strongloop/loopback-next-example/blob/master/services/account-without-juggler/repositories/account/models/Account.ts) + ``` + +#### Property Decorator + + Syntax: `@property(definition?: PropertyDefinition)` + + The property decorator defines metadata for a property on a Model definition. + The format of property definitions can be found in [Property definitions](../lb2/Model-definition-JSON-file.html#properties) + + For usage examples, see [Define Models](Repositories.html#define-models) + +### Relation Decorators + + The relation decorator defines the nature of a relationship between two models. + + *This feature has not yet been released in alpha form. Documentation will be* + *added here as this feature progresses.* + +#### Relation Decorator + + Syntax: `@relation` + + Register a general relation. + +#### Specfic Relation Decorator + + Syntax: + + - `@belongsTo` + - `@hasOne` + - `@hasMany` + - `@embedsOne` + - `@embedsMany` + - `@referencesOne` + - `@referencesMany` + + Register a specific relation + +### Repository Decorator + + Syntax: `@repository(model: string | typeof Entity, dataSource?: string | juggler.DataSource)` + + This decorator either injects an existing repository or creates a repository + from a model and a datasource. + + The injection example can be found in [Repository#controller-configuration](Repositories.html#controller-configuration) + + To create a repository in a controller, you can define your model and datasource + first, then import them in your controller file: + + *To learn more about creating model and datasource, please see the example in [Thinking in LoopBack](Thinking-in-LoopBack.htm#define-product-model-repository-and-data-source)* + + ```ts + // my-controller.ts + import { Todo } from '{path_of_Todo_model}.ts'; + import { datasource } from '{path_of_datasource}.ts'; + + export class TodoController { + @repository(Todo, datasource) + repository: EntityCrudRepository; + ... ... + } + ``` + + If the model or datasource is already bound to the app, you can create the + repository by providing their names instead of the classes. For example: + + ```ts + // with `datasource` and `Todo` already defined. + app.bind('datasources.ds').to(datasource); + app.bind('repositories.todo').to(Todo); + + export class TodoController { + @repository('todo', 'ds') + repository: EntityCrudRepository; + // etc + } + ``` diff --git a/docs/Defining-and-validating-the-API.md b/docs/Defining-and-validating-the-API.md new file mode 100644 index 000000000000..1e4a02aa56da --- /dev/null +++ b/docs/Defining-and-validating-the-API.md @@ -0,0 +1,351 @@ +--- +lang: en +title: 'Defining and validating the API' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Defining-and-validating-the-API.html +summary: +--- +{% include important.html content="The top-down approach for building LoopBack +applications is not yet fully supported. Therefore, the steps outlined in this +page are outdated and may not work out of the box. They will be revisited after +our MVP release. +"%} + +## Define the API + +### Start with data + +When building an API, its usually easiest to start by outlining some example data that consumers of the API will need. This can act as the first rough draft of the API specification for smaller applications / APIs. In this tutorial, you'll start by sketching out some example API response data as simple JavaScript objects: + +```js +const products = [ + {name: 'Headphones', price: 29.99, category: '/categories/accessories', available: true, deals: ['50% off', 'free shipping']}, + {name: 'Mouse', price: 32.99, category: '/categories/accessories', available: true, deals: ['30% off', 'free shipping']}, + {name: 'yPhone', price: 299.99, category: '/categories/phones', available: true, deals: ['free shipping']}, + {name: 'yBook', price: 5999.99, category: '/categories/computers', available: true} +]; +``` + +With the example data defined, you can start to get an idea of how to separate the data into individual proper nouns, which will eventually be defined in different ways. Either as resources, schemas, models, or repositories. + +- `CatalogItem` - Each object in the array above +- `Category` - Has a URL, and more information about the category +- `Product` - The name, price and other product information +- `Inventory` - The product availability +- `Deals` - Information about promotions on a group of products + +### Outline the API +With the proper nouns of the API defined, you can now start to think about what the API will look like. + +This is where you choose how fine or coarse grain the API will be. You have to decide which proper nouns above will be available as _Resources_. The easiest way to figure out which Resources are needed is by sketching out the URLs (without verbs) for the API: + + - `/products?{query}` - Search for products in the catalog + - `/product/{slug}` - Get the details for a particular product + - `/deals?{query}` - Search for deals + - `/deal/{slug}` - Get the details for a particular deal + - `/categories?{query}` - Search for categories + - `/category/{slug}` - Get the details for a particular category + - `/category/{slug}/products?{query}` - Search for products in a particular category + +### Break down the data into resources +With the URLs, defined, its easy to determine which resources you'll need. + + - `ProductResource` + - `DealResource` + - `CategoryResource` + +This is where it's useful to determine similarities between Resources; for example, the `ProductResource`, `DealResource`, and `CategoryResource` all have the same URL structure, with the exception of `/category/{slug}/products?{query}` path on `CategoryResource`: + + - `/{pluralName}?{query}` - Search with a query and the resource plural name + - `/{name}/{slug}` - Get details about the resource + +### Using patterns to reduce duplication + +It can be tricky to determine the patterns on which to base the API, since you'll likely want to change it in the future. To keep the patterns flexible, you can define these patterns via simple TypeScript functions (you can also do it in JavaScript). Start with a `SearchableResource` pattern, since all of the resources must support the same search and listing operations. + +The `SearchableResource` pattern will define all of the semantics for an OpenAPI fragment that supports search. + +{% include code-caption.html content="/apidefs/templates/searchable-resource.ts" %} + +```ts +export let searchableResource = (resource: any, type: string) => ({ + paths: { + [`/${resource.path}`]: { // pattern + get: { + "parameters": [{ + in: "query", + name: "filter", + type: "string", + }], + "responses": { + 200: { + description: resource.description || + `Result set of type ${type} returned.`, + schema: { + $ref: `#/definitions/${type}`, + type: "array", + }, + }, + }, + "x-controller-name": resource.controller, + "x-operation-name": "search", + }, + }, + [`/${resource.path}/{slug}`]: { // pattern + get: { + "parameters": [ + { + in: "path", + name: "slug", + required: true, + type: "string", + }, + ], + "responses": { + 200: { + description: resource.description || + `Result of type ${type} returned.`, + schema: { + $ref: `#/definitions/${type}`, + }, + }, + }, + "x-controller-name": resource.controller, + "x-operation-name": "getDetails", + }, + }, + }, +}); +``` + +Here's another example for creating a POST template, called `CreatableResource`. + +{% include code-caption.html content="/apidefs/templates/creatable-resource.ts" %} + +```ts +export let creatableResource = (resource: any, type: string) => ({ + paths: { + [`/${resource.path}`]: { // pattern + post: { + "parameters": [ + { + in: "body", + name: "body", + required: true, + schema: { + $ref: `#/definitions/${type}`, + }, + }, + ], + "responses": { + 201: { + description: resource.description + || `The ${type} instance was created.`, + schema: { + $ref: `#/definitions/${type}`, + }, + }, + }, + "x-controller-name": resource.controller, + "x-operation-name": "create", + }, + }, + }, +}); +``` + +Lastly, you'll create a helper function for generating type definitions in +OpenAPI. + +{% include code-caption.html content="/apidefs/templates/type-definition.ts" %} + +```ts +import { DefinitionsObject } from "@loopback/openapi-spec"; + +export let TypeDefinition = (type: any): DefinitionsObject => ({ + definitions: { + [`${type.name}`]: { + properties: type.definition, + }, + }, +}); +``` + +Given the pattern function above, you can now create the OpenAPI fragment that +represents the `ProductController` portion of the full specification. +This example, uses [lodash](https://lodash.com/) to help with merging generated definitions together. Install lodash with this command: + +```shell +npm install --save lodash +``` + +{% include code-caption.html content="/apidefs/product.api.ts" %} + +```ts +import * as _ from "lodash"; + +// Assuming you have created the "base" schema elsewhere. +// If there are no common properties between all of the endpoint objects, +// then you can ignore this. +import BaseSchema from "../BaseSchema"; +// Don't forget to export the template functions under a common file! +import { SearchableResource, CreatableResource, TypeDefinition } from "./templates"; +let ProductAPI: ControllerSpec = {}; + +const ProductDefinition = {}; +// Build type definition using base schema + additional properties. +_.merge(ProductDefinition, BaseSchema, TypeDefinition({ + price: { + type: "number", + minimum: 0, + exclusiveMinimum: true, + } +})); + +const ProductGetResource = SearchableResource({ + controller: "ProductController", + operation: "search", + path: "products", +}, "Product"); + +const ProductCreateResource = CreatableResource({ + controller: "ProductController", + operation: "create", + path: "products", +}, "Product"); +// Rinse and repeat for PUT, PATCH, DELETE, etc... + +// Merge all of the objects together. +// This will mix the product definition into the "definitions" property of the +// OpenAPI spec, and the resources will be mixed into the "paths" property. +_.merge(ProductAPI, ProductDefinition, ProductGetResource, + ProductCreateResource); + +// And export it! +export default ProductAPI; +``` + +### Connect OpenAPI fragments to Controllers + +By separating each individual "Model"-level API export, you can link +them to their corresponding controllers throughout the application. + +{% include code-caption.html content="/controllers/product-controller.ts" %} + +```ts +import { api } from "@loopback/core"; +import ProductApi from "../apidefs/product.api"; + +// This decorator binds the Product API to the controller, +// which will establish routing to the specified functions below. +@api(ProductApi) +export class ProductController { + + // Note that the function names here match the strings in the "operation" + // property you provided to the SearchableResource call in the previous + // example. + public search() { + // your logic here + } + + // Same goes for this function! + public create(id: number, name: string, price: number) { + // your logic here + } + + // etc... +} +``` + +### Putting together the final API specification + +Now that you've built the OpenAPI fragments for each of the controllers, +you can put them all together to produce the final OpenAPI spec. + +{% include code-caption.html content="/apidefs/swagger.ts" %} + +```ts +import { ProductAPI, DealAPI, CategoryAPI } from "../apidefs"; +import * as OpenApiSpec from "@loopback/openapi-spec"; +import * as _ from "lodash"; + + +// Import API fragments here + +export let spec = OpenApiSpec.createEmptyApiSpec(); +spec.info = { + title: "Your API", + version: "1.0", +}; +spec.swagger = "2.0"; +spec.basePath = "/"; + +_.merge(spec, ProductAPI); +_.merge(spec, DealAPI); +_.merge(spec, CategoryAPI); + +export default spec; +``` + +You can then bind the full spec to the application using `server.spec()`. This is done on the server level, because each server instance can expose a different (sub)set of API. + +You also need to associate the controllers implementing the spec with the app using `app.controller(GreetController)`. This is not done on the server level because a controller may be used with multiple server instances, and types! + +```ts +// application.ts +// This should be the export from the previous example. +import spec from "../apidefs/swagger"; +import { RestApplication, RestServer } from "@loopback/rest"; +import { ProductController, DealController, CategoryController } from "./controllers"; +export class YourMicroservice extends RestApplication { + + constructor() { + super(); + const app = this; + + app.controller(ProductController); + app.controller(DealController); + app.controller(CategoryController); + + } + async start() { + const server = await app.getServer(RestServer); + // inject your spec here! + server.api(spec); + server.bind("rest.port").to(3001); + await super.start(); + } + // etc... +} +``` + +## Validate the API specification + +[The OpenAPI Swagger editor](https://editor.swagger.io) is a handy tool for editing OpenAPI specifications that comes with a built-in validator. It can be useful to manually validate an OpenAPI specification. + +However, manual validation is tedious and error prone. It's better to use an automated solution that's run as part of a CI/CD workflow. LoopBack's `testlab` module provides a helper function for checking whether a specification conforms to OpenAPI Spec. Just add a new Mocha test that calls this helper function to the test suite: + +```ts +// test/acceptance/api-spec.acceptance.ts + +import {validateApiSpec} from '@loopback/testlab'; +import {MyApp} from '../..'; +import {RestServer} from '@loopback/rest'; + +describe('API specification', () => { + it('api spec is valid', async () => { + const app = new MyApp(); + const server = await app.getServer(RestServer); + const spec = server.getApiSpec(); + await validateApiSpec(apiSpec); + }); +}); +``` + +See [Validate your OpenAPI specification](Testing-your-application.html#validate-your-openapi-specification) from [Testing your application](Testing-your-application.md) for more details. + +{% include next.html content= " +[Testing the API](./Testing-the-API.md) +" %} diff --git a/docs/Defining-your-testing-strategy.md b/docs/Defining-your-testing-strategy.md new file mode 100644 index 000000000000..837e7340f208 --- /dev/null +++ b/docs/Defining-your-testing-strategy.md @@ -0,0 +1,76 @@ +--- +lang: en +title: 'Defining your testing strategy' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Defining-your-testing-strategy.html +summary: +--- + +{% include previous.html content=" +This article continues off from [Testing the API](./Testing-the-API.md). +" %} + +## Define your testing strategy + +It may be tempting to overlook the importance of a good testing strategy when starting a new project. Initially, as the project is small and you mostly keep adding new code, even a badly-written test suite seems to work well. However, as the project grows and matures, inefficiencies in the test suite can severely slow down progress. + +A good test suite has the following properties: + + - **Speed**: The test suite should complete quickly. This encourages short red-green-refactor cycle, which makes it easier to spot problems, because there have been few changes made since the last test run that passed. It also shortens deployment times, making it easy to frequently ship small changes, reducing the risk of major breakages. + - **Reliability**: The test suite should be reliable. No developer enjoys debugging a failing test only to find out it was poorly written and failures are not related to any problem in the tested code. Flaky tests reduce the trust in your tests, up to point where you learn to ignore these failures, which will eventually lead to a situation when a test failed legitimately because of a bug in the application, but you did not notice. + - **Isolation of failures**: The test suite should make it easy to isolate the source of test failures. To fix a failing test, developers need to find the specific place that does not work as expected. When the project contains thousands of lines and the test failure can be caused by any part of the system, then finding the bug is very difficult, time consuming and demotivating. + - **Resilience**: The test implementation should be robust and resilient to changes in the tested code. As the project grows and matures, you may need to change existing behavior. With a brittle test suite, each change may break dozens of tests, for example when you have many end-to-end/UI tests that rely on specific UI layout. This makes change prohibitively expensive, up to a point where you may start questioning the value of such test suite. + +References: + +- [Test Pyramid](https://martinfowler.com/bliki/TestPyramid.html) by Martin Fowler +- [The testing pyramid](http://www.agilenutshell.com/episodes/41-testing-pyramid) by Jonathan Rasmusson +- [Just say no to more end-to-end tests](https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html) +- [100,000 e2e selenium tests? Sounds like a nightmare!](https://watirmelon.blog/2014/05/14/100000-e2e-selenium-tests-sounds-like-a-nightmare/) +- [Growing Object-Oriented Software Guided by Tests](http://www.growing-object-oriented-software.com/) + +### How to build a great test suite + +To create a great test suite, think smaller and favor fast, focused unit-tests over slow application-wide end-to-end tests. + +Say you are implementing the "search" endpoint of the Product resource described earlier. You might write the following tests: + + 1. One "acceptance test", where you start the application, make an HTTP request to search for a given product name, and verify that expected products were returned. This verifies that all parts of the application are correctly wired together. + + 2. Few "integration tests" where you invoke `ProductController` API from JavaScript/TypeScript, talk to a real database, and verify that the queries built by the controller work as expected when executed by the database server. + + 3. Many "unit tests" where you test `ProductController` in isolation and verify that the controller handles all different situations, including error paths and edge cases. + +### Testing workflow + +Here is what your testing workflow might look like: + + 1. Write an acceptance test demonstrating the new feature you are going to build. Watch the test fail with a helpful error message. Use this new test as a reminder of what is the scope of your current work. When the new tests passes then you are done. + + 2. Think about the different ways how the new feature can be used and pick one that's most easy to implement. Consider error scenarios and edge cases that you need to handle too. In the example above, where you want to search for products by name, you may start with the case when no product is found. + + 3. Write a unit-test for this case and watch it fail with an expected (and helpful) error message. This is the "red" step in Test Driven Development ([TDD](https://en.wikipedia.org/wiki/Test-driven_development)). + + 4. Write a minimal implementation need to make your tests pass. Building up on the example above, let your search method return an empty array. This is the "green" step in TDD. + + 5. Review the code you have written so far, and refactor as needed to clean up the design. Don't forget to keep your test code clean too! This is the "refactor" step in TDD. + + 6. Repeat the steps 2-5 until your acceptance test starts passing. + +When writing new unit tests, watch out for situations where your tests are asserting on how the tested objects interacted with the mocked dependencies, while making implicit assumptions about what is the correct usage of the dependencies. This may indicate that you should add an integration test in addition to a unit test. + +For example, when writing a unit test to verify that the search endpoint is building a correct database query, you would usually assert that the controller invoked the model repository method with an expected query. While this gives us confidence about the way the controller is building queries, it does not tell us whether such queries will actually work when they are executed by the database server. An integration test is needed here. + +To summarize: + +- Pay attention to your test code. It's as important as the "real" code you are shipping to production. +- Prefer fast and focused unit tests over slow app-wide end-to-end tests. +- Watch out for integration points that are not covered by unit-tests and add integration tests to verify your units work well together. + +See [Testing Your Application](Testing-Your-application.md) for a reference manual on automated tests. + +{% include next.html content= " +[Implementing features](./Implementing-features.md) +" %} diff --git a/docs/Dependency-injection.md b/docs/Dependency-injection.md new file mode 100644 index 000000000000..1413c76f66fc --- /dev/null +++ b/docs/Dependency-injection.md @@ -0,0 +1,251 @@ +--- +lang: en +title: 'Dependency injection' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Dependency-injection.html +summary: +--- +## Introduction + +[Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) is a technique where the construction of dependencies of a class or function is separated from its behavior, in order to keep the code [loosely coupled](https://en.wikipedia.org/wiki/Loose_coupling). + +For example, the Sequence Action `authenticate` supports different authentication strategies (e.g. HTTP Basic Auth, OAuth2, etc.). Instead of hard-coding some sort of a lookup table to find the right strategy instance, `authenticate` uses dependency injection to let the caller specify which strategy to use. + +```ts +class AuthenticationProvider { + constructor( + @inject('authentication.strategy') + strategy + ) { + this.strategy = strategy; + } + + value() { + // this is the function invoked by "authenticate()" sequence action + return async (request) => { + const adapter = new StrategyAdapter(this.strategy); + const user = await adapter.authenticate(request); + return user; + } + } +} +``` + +Dependency Injection makes the code easier to extend and customize, because the dependencies can be easily rewired by the application developer. It makes the code easier to test in isolation (in a pure unit test), because the test can inject a custom version of the dependency (a mock or a stub). This is especially important when testing code interacting with external services like a database or an OAuth2 provider. Instead of making expensive network requests, the test can provide a lightweight implementation returning pre-defined responses. + +## Configuring what to inject + +Now that we write a class that gets the dependencies injected, you are probably wondering where are these values going to be injected from and how to configure what should be injected. This part is typically handled by an IoC Container, where IoC means [Inversion of Control](https://en.wikipedia.org/wiki/Inversion_of_control). + +In LoopBack, we use [Context](Context.md) to keep track of all injectable dependencies. + +There are several different ways for configuring the values to inject, the simplest options is to call `app.bind(key).to(value)`. Building on top of the example above, one can configure the app to use a Basic HTTP authentication strategy as follows: + +```ts +// TypeScript example + +import {BasicStrategy} from 'passport-http'; +import {RestApplication, RestServer} from '@loopback/rest'; +// basic scaffolding stuff happens in between... + +const server = await app.getServer(RestServer); // The REST server has its own context! +server.bind('authentication.strategy').to(new BasicStrategy(loginUser)); + +function loginUser(username, password, cb) { + // check that username + password are valid +} +``` + +However, when you want to create a binding that will instantiate a class and automatically inject required dependencies, then you need to use `.toClass()` method: + +```ts +server.bind('authentication.provider').toClass(AuthenticationProvider); + +const provider = await server.get('authentication.provider'); +// provider is an AuthenticationProvider instance +// provider.strategy was set to the value returned by server.get('authentication.strategy') +``` + +When a binding is created via `.toClass()`, [Context](Context.md) will create a new instance of the class when resolving the value of this binding, injecting constructor arguments and property values as configured via `@inject` decorator. + +Note that the dependencies to be injected could be classes themselves, in which case [Context](Context.md) will recursively instantiate these classes first, resolving their dependencies as needed. + +In this particular example, the class is a [Provider](Writing-Components#providers). Providers allow you to customize the way how a value is created by the Context, possibly depending on other Context values. A provider is typically bound using `.toProvider(.md)` API: + +```js +app.bind('authentication.provider').toProvider(AuthenticationProvider); + +const authenticate = await app.get('authentication.provider'); + +// authenticate is the function returned by provider's value() method +``` + +You can learn more about Providers in [Creating Components](Creating-components.md). +## Flavors of Dependency Injection + +LoopBack supports three kinds of dependency injection: + + 1. constructor injection: the dependencies are provided as arguments of the class constructor. + 2. property injection: the dependencies are stored in instance properties after the class was constructed. + 3. method injection: the dependencies are provided as arguments of a method invocation. Please note that constructor injection is a special form of method injection to instantiate a class by calling its constructor. + +### Constructor injection + +This is the most common flavor that should be your default choice. + +```js +class ProductController { + constructor( + @inject('repositories.Product') + repo + ) { + this.repo = repo; + } + + async list() { + return await this.repo.find({where: {available: true}}); + } +} +``` + +### Property injection + +Property injection is usually used for optional dependencies which are not required for the class to function or for dependencies that have a reasonable default. + +```ts +class InfoController { + @inject('logger') + private logger = ConsoleLogger(); + + status() { + this.logger.info('Status endpoint accessed.'); + return {pid: process.pid}; + } +} +``` + +### Method injection + +Method injection allows injection of dependencies at method invocation level. The parameters are decorated +with `@inject` or other variants to declare dependencies as method arguments. + +```ts +class InfoController { + + greet(@inject('authentication.currentUser') user: UserProfile) { + return `Hello, ${userProfile.name}`; + } +} +``` + +## Optional dependencies + +Sometimes the dependencies are optional. For example, the logging level for a Logger provider can have a default value if it is not set (bound to the context). + +To resolve an optional dependency, set `optional` flag to true: + +```ts +const ctx = new Context(); +await ctx.get('optional-key', {optional: true}); // Return `undefined` instead of throwing an error +``` + +Here is another example showing optional dependency injection using properties with default values: + +```ts +// Optional property injection +export class LoggerProvider implements Provider { + // Log writer is an optional dependency and it falls back to `logToConsole` + @inject('log.writer', {optional: true}) + private logWriter: LogWriterFn = logToConsole; + + // Log level is an optional dependency with a default value `WARN` + @inject('log.level', {optional: true}) + private logLevel: string = 'WARN'; +} +``` + +Optional dependencies can also be used with constructor and method injections. For example: + +```ts +// Optional constructor injection +export class LoggerProvider implements Provider { + constructor( + // Log writer is an optional dependency and it falls back to `logToConsole` + @inject('log.writer', {optional: true}) + private logWriter: LogWriterFn = logToConsole, + + // Log level is an optional dependency with a default value `WARN` + @inject('log.level', {optional: true}) + private logLevel: string = 'WARN', + ) {} +} +``` + +```ts +// Optional method injection +export class MyController { + // prefix is optional + greet(@inject('hello.prefix', {optional: true}) prefix: string = 'Hello') { + return `${prefix}, world!`; + } +} +``` + +## Circular dependencies + +LoopBack can detect circular dependencies and report the path which leads to the problem. +For example, + +```ts +import {Context, inject} from '@loopback/context'; + +interface Developer { + // Each developer belongs to a team + team: Team; +} + +interface Team { + // Each team works on a project + project: Project; +} + +interface Project { + // Each project has a lead developer + lead: Developer; +} + +class DeveloperImpl implements Developer { + constructor(@inject('team') public team: Team) {} +} + +class TeamImpl implements Team { + constructor(@inject('project') public project: Project) {} +} + +class ProjectImpl implements Project { + constructor(@inject('lead') public lead: Developer) {} +} + +const context = new Context(); + +context.bind('lead').toClass(DeveloperImpl); +context.bind('team').toClass(TeamImpl); +context.bind('project').toClass(ProjectImpl); + +try { + // The following call will fail + context.getSync('lead'); +} catch (e) { + console.error(e.toString()); + // Error: Circular dependency detected: lead --> @DeveloperImpl.constructor[0] + // --> team --> @TeamImpl.constructor[0] --> project --> @ProjectImpl.constructor[0] + // --> lead +} +``` + +## Additional resources + + - [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) on Wikipedia + - [Dependency Inversion Principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) on Wikipedia diff --git a/docs/Download-examples.md b/docs/Download-examples.md new file mode 100644 index 000000000000..107be4e9c29e --- /dev/null +++ b/docs/Download-examples.md @@ -0,0 +1,44 @@ +--- +lang: en +title: 'Download examples' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Download-examples.html +summary: +--- + +### Synopsis + +Downloads a LoopBack example project from our +[GitHub monorepo](https://github.com/strongloop/loopback-next). + + +```text +lb4 example [options] [] +``` + +### Options + +{% include_relative includes/CLI-std-options.md %} + +### Arguments + +`example-name` - Optional name of the example to clone. If provided, the tool +will skip the example-name prompt and run in a non-interactive mode. + +See [Examples and tutorials](Examples-and-tutorials.md) for the list of +available examples. + +### Interactive Prompts + +The tool will prompt you for: + +- Name of the example to download. If the name had been supplied from the +command-line, the prompt is skipped. + +### Output + +The example project is downloaded to a new directory. For example, when +downloading `getting-started` example, the tool stores the files +under the newly created `loopback4-example-getting-started` directory. diff --git a/docs/Examples-and-tutorials.md b/docs/Examples-and-tutorials.md new file mode 100644 index 000000000000..ab45944748be --- /dev/null +++ b/docs/Examples-and-tutorials.md @@ -0,0 +1,37 @@ +--- +lang: en +title: Examples and tutorials +keywords: LoopBack 4.0 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Examples-and-tutorials.html +summary: +--- + +LoopBack 4 comes with the following example projects: + +- **[hello-world](https://github.com/strongloop/loopback-next/tree/master/packages/example-hello-world)**: + Tutorial on setting up a simple hello-world application using LoopBack 4. + +- **[getting-started](https://github.com/strongloop/loopback-next/tree/master/packages/example-getting-started)**: + Tutorial on building a simple application with LoopBack 4 key concepts. + +- **[log-extension](https://github.com/strongloop/loopback-next/tree/master/packages/example-log-extension)**: + Tutorial on building a log extension. + +- **[rpc-server](https://github.com/strongloop/loopback-next/tree/master/packages/example-rpc-server)**: + An example showing how to implement a made-up RPC protocol. + +You can download any of the example projects usig our CLI tool `lb4`: + + +``` +$ lb4 example +? What example would you like to clone? (Use arrow keys) +❯ getting-started: An application and tutorial on how to build with LoopBack 4. + hello-world: A simple hello-world Application using LoopBack 4 + log-extension: An example extension project for LoopBack 4 + rpc-server: A basic RPC server using a made-up protocol. +``` + +Please follow the instructions in [Install LoopBack4 CLI](Getting-started.html#install-loopback-4-cli) if you don't have `lb4` installed yet. diff --git a/docs/Extending-LoopBack-4.md b/docs/Extending-LoopBack-4.md new file mode 100644 index 000000000000..26ccc6c748be --- /dev/null +++ b/docs/Extending-LoopBack-4.md @@ -0,0 +1,158 @@ +--- +lang: en +title: 'Extending LoopBack 4' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Extending-LoopBack-4.html +summary: +--- + +## Overview + +LoopBack 4 is designed to be highly extensible. For architectural rationale and motivation, see [Crafting LoopBack 4](Crafting-LoopBack-4.md). + +## Building blocks for extensibility + +The [@loopback/context](https://github.com/strongloop/loopback-next/tree/master/packages/context) module implements an [Inversion of Control](https://en.wikipedia.org/wiki/Inversion_of_control) (IoC) container called [Context](Context.md) as a service registry that supports [Dependency injection](Dependency-injection.md). + +The IoC container decouples service providers and consumers. A service provider can be bound to the context with a key, which can be treated as an address of the service provider. + +The diagram below shows how the Context manages services and their dependencies. + +![loopback-ioc](/images/lb4/loopback-ioc.png) + +In the example above, there are three services in the Context and each of them are bound to a unique key. + +- *controllers.UserController*: A controller to implement user management APIs +- *repositories.UserRepository*: A repository to provide persistence for user records +- *utilities.PasswordHasher*: A utility function to hash passwords + +Please also note that `UserController` depends on an instance of `UserRepository` and `PasswordHasher`. Such dependencies are also managed by the Context to provide composition capability for service instances. + +Service consumers can then either locate the provider using the binding key or declare a dependency using `@inject('binding-key-of-a-service-provider')` so that the service provider can be injected into the consumer class. The code snippet below shows the usage of `@inject` for dependency injection. + +```ts +import {inject, Context} from '@loopback/context'; + +/** + * A UserController implementation that depends on UserRepository and PasswordHasher + */ +class UserController { + // UserRepository and PasswordHasher are injected via the constructor + constructor( + @inject('repositories.UserRepository') private userRepository: UserRepository, + @inject('utilities.PasswordHasher') private passwordHasher: PasswordHasher), + ) {} + + /** + * Login a user with name and password + */ + async login(userName: string, password: String): boolean { + const hash = this.passHasher.hash(password); + const user = await this.userRepository.findById(userName); + return user && user.passwordHash === hash; + } +} + +const ctx = new Context(); +// Bind repositories.UserRepository to UserRepository class +ctx.bind('repositories.UserRepository').toClass(MySQLUserRepository); +// Bind utilities.PasswordHash to a function +ctx.bind('utilities.PasswordHash').to((password) => { /* ... */ }) +// Bind the UserController class as the user management implementation +ctx.bind('controllers.UserController').toClass(UserController); + +// Locate the an instance of UserController from the context +const userController: UserController = await ctx.get('controller.UserController'); +// Run the log() +const ok = await logger.login('John', 'MyPassWord'); +``` + +Now you might wonder why the IoC container is fundamental to extensibility. Here's how it's achieved. + +1. An alternative implementation of the service provider can be bound the context to replace the existing one. For example, we can implement different hashing functions for password encryption. The user management system can then receive a custom password hashing. + +2. Services can be organized as extension points and extensions. For example, to allow multiple authentication strategies, the `authentication` component can define an extension point as `authentication-manager` and various authentication strategies such as user/password, LDAP, oAuth2 can be contributed to the extension point as extensions. The relation will look like: + +![loopback-extension](/images/lb4/loopback-extension.png) + +To allow a list of extensions to be contributed to LoopBack framework and applications, we introduce `Component` as the packaging model to bundle extensions. A component is either a npm module or a local folder structure that contains one or more extensions. It's then exported as a class implementing the `Component` interface. For example: + +```ts +... +import {Component, ProviderMap} from '@loopback/core'; + +export class UserManagementComponent implements Component { + providers?: ProviderMap; + + constructor() { + this.controllers = { + [UserBindings.CONTROLLER]: UserController, + }; + this.repositories = { + [UserBindings.REPOSITORY]: UserRepository, + }; + } +} +``` + +The interaction between the application context and `UserManagement` component is illustrated below: + +![loopback-component](/images/lb4/loopback-component.png) + +For more information about components, see: + +- [Creating components](Creating-components.md) +- [Using Components](Using-components.md) + +## Types of extensions + +- Binding providers +- Decorators +- Sequence & Actions +- Connectors +- Utility functions +- Controllers +- Repositories +- Models +- Mixins + +For a list of candidate extensions, see [loopback-next issue #512](https://github.com/strongloop/loopback-next/issues/512). + +### System vs Application extensions + +Some extensions are meant to extend the programming model and integration capability of the LoopBack 4 framework. Good examples of such extensions are: + +- Binding providers +- Decorators +- Sequence & Actions +- Connectors +- Utility functions +- Mixins (for application) + +An application may consist of multiple components for the business logic. For example, an online shopping application typically has the following component: + +- UserManagement +- ShoppingCart +- AddressBook +- OrderManagement + +An application-level component usually contributes: + +- Controllers +- Repositories +- Models +- Mixins (for models) + +## How to build my own extensions + +### Learn from existing ones + +- [loopback4-example-log-extension](https://github.com/strongloop/loopback4-example-log-extension) +- [@loopback/authentication](https://github.com/strongloop/loopback-next/tree/master/packages/authentication) + +### Create your own from the starter + +The [loopback4-extension-starter](https://github.com/strongloop/loopback4-extension-starter) project provides a template to create your own LoopBack 4 extensions as a component. Please follow the instructions to get started. + diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 000000000000..82887c56e119 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,66 @@ +--- +lang: en +title: 'Frequently-asked questions' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/FAQ.html +summary: LoopBack 4 is a completely new framework, sometimes referred to as LoopBack-Next. + +--- +### What’s the vision behind LoopBack 4? + +- Make it even easier to build apps that require complex integrations +- Enabling an ecosystem of extensions +- Small, fast, flexible, powerful core +- Suitable for small and large teams +- Minimally opinionated, enforce your team's opinions instead + +### What’s the timeline for LoopBack 4? + +See [Upcoming releases](https://github.com/strongloop/loopback-next/wiki/Upcoming-Releases). + +### Where are the tutorials? + +See [Examples and tutorials](Examples-and-tutorials.md). + +### What features are planned ? + +- 100% promise-based APIs and async and await as first-class keywords. +- Being able to choose to use JavaScript or TypeScript. +- Better extensibility, ability to override any aspect of the framework (for example, no more built-in User - model pain, easily replace parts of ACL with your own). +- Define APIs / remote methods with OpenAPI (Swagger). +- Organize business and other logic into controllers with their own opinionated API or generate an PersistedModel style API. +- Better routing performance +- React SDK +- Create GraphQL based APIs +- GraphQL => juggler integration +- Advanced declarative caching support +- New DSL for defining APIs / Models +- Completely new tooling w/ Visual Studio Code integration +- More at [Feature proposals](https://github.com/strongloop/loopback-next/wiki/Feature-proposals) + +Add your feature requests at [loopback-next/issues/new](https://github.com/strongloop/loopback-next/issues/new). + +### Why TypeScript? + +Although developers can still write application logic in either JavaScript or TypeScript, LoopBack 4's core is written in TypeScript, for the following reasons: + +- **Improved developer productivity and scalability**. Our customers need a framework that scales to dozens and even hundreds of developers. This scalability is the reason TypeScript exists and is gaining traction. +- **Improved extensibility** and flexibility. LoopBack 4's core is simpler than LoopBack 3.x with well-defined extension points. A lot of responsibility will be shifted to extensions (componnets), which can be JavaScript or TypeScript. +- Unified tooling. TypeScript developers all use the same IDE: Visual Studio Code. The LoopBack ecosystem could someday be filled with useful best practices around that IDE and even great developer plugins. Right now that effort is split between various editors and basically non-existent. +- **Future-proofing**. Ability to leverage the latest and future JavaScript constructs. + +TypeScript's support for static analysis makes more robust tooling possible and is the foundation for its scalability. The ability to easily refactor code without the common human-introduced errors. Dev and Compile time checking. For example, most people don't have the same expertise and time we do to setup complex linting solutions (for example, a linting config that works across many projects). + +For more details, see the lengthy discussion in [#6](https://github.com/strongloop/loopback-next/issues/6). + +### Does JavaScript still work? + +LoopBack 4 itself is written in [TypeScript](https://www.typescriptlang.org) (that compiles to JavaScript), but it supports applications written in both TypeScript and JavaScript. The documentation assumes you have a basic understanding of the JavaScript language; and when it says "JavaScript" it is understood to mean ECMAScript version 6 (ES6). + +Some of the examples use ES6 syntax. We encourage you to get familiar with ES6 constructs such as arrow functions, classes, template literals, let, and const statements. + +### LoopBack 3 vs LoopBack 4 + +See [Differences between LoopBack v3 and v4](LoopBack-3.x.md). diff --git a/docs/Getting-started.md b/docs/Getting-started.md new file mode 100644 index 000000000000..8cc92aded5ba --- /dev/null +++ b/docs/Getting-started.md @@ -0,0 +1,90 @@ +--- +lang: en +title: 'Getting started' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Getting-started.html +summary: Write and run a LoopBack 4 "Hello World" project in JavaScript and TypeScript. +--- +## Prerequisites + +Install [Node.js](https://nodejs.org/en/download/) (version 8.x.x or higher) if +not already installed on your machine. + +## Install LoopBack 4 CLI + +The LoopBack 4 CLI is a command-line interface that can scaffold a project or +extension with more features under development. CLI provides the fastest way to +get started with a LoopBack 4 project that adheres to best practices. + +Install the CLI globally by running +```sh +npm i -g @loopback/cli +``` + +## Create a new project + +The CLI tool will scaffold the project, configure TypeScript compiler and +install all the required dependencies. To create a new project, run the CLI as +follows and answer the prompts. +```sh +lb4 app +``` + +Answer the prompts as follows: +```sh +? Project name: getting-started +? Project description: Getting started tutorial +? Project root directory: (getting-started) +? Application class name: StarterApplication +? Select project build settings: Enable tslint, Enable prettier, Enable mocha, Enable loopbackBuild +``` + +### Starting the project + +The project comes with a "ping" route to test the project. Let's try it out by running the project. +```sh +cd getting-started +npm start +``` + +In a browser, visit [http://127.0.0.1:3000/ping](http://127.0.0.1:3000/ping). + +## Adding your own controller + +Now that we have a basic project created, it's time to add our own [controller](Controllers.md). +Let's add a simple "Hello World" controller as follows: + +* Create a new file in `/src/controllers/` called `hello.controller.ts`. + +* Paste the following contents into the file: + ```ts + import {get} from '@loopback/rest'; + + export class HelloController { + @get('/hello') + hello(): string { + return 'Hello world!'; + } + } + ``` + +* Update `/src/application.ts` to load the controller: + * Import `HelloController` at the top of the file + ```ts + import {HelloController} from './controllers/hello.controller'; + ``` + + * Add controller in `setupControllers()` + ```ts + setupControllers() { + this.controller(PingController); + this.controller(HelloController); + } + ``` + +* Start the application using `npm start`. + * *Note: If your application is still running, press **CTRL+C** to stop it before restarting it* + +* Visit [http://127.0.0.1:3000/hello](http://127.0.0.1:3000/hello) to see `Hello world!` diff --git a/docs/Glossary.md b/docs/Glossary.md new file mode 100644 index 000000000000..1ac07c6c6c24 --- /dev/null +++ b/docs/Glossary.md @@ -0,0 +1,27 @@ +**Action**: JavaScript functions that only accept or return Elements. Since the input of one action (an Element) is the output of another action (Element) they are easily composed. + +**API specification**: An [OpenAPI](https://www.openapis.org) document (in YAML or JSON format) that describes a REST API. It specifies the metadata (verbs, paths, headers, and so on) a client needs to make a valid request to the API. + +**Application**: A container of components. + +**Component**: A reusable bundle of Bindings, [Controllers](Controllers.md), Services, [Repositories](Repositories.md), and models. For more information, see [Using components](Using-components.md) and [Creating components](Creating-components.md). + +**Connector**: An interface that abstracts underlying backend systems (for example, database, web service, and so on). + +**Context**: An encapsulation of request and response objects provides useful values for writing web applications and APIs. For more information, see [Context](Context.md). + +**Controller**: The implementation of API endpoints. + +**DataSource**: A named configuration for a Connector instance that represents data in an external system. + +**Element:** The building blocks of a Sequence, such as route, params, and result. For more information, see [Sequence](Sequence.md#elements). + +**Mixin**: An interface for models. + +**Model**: Defines application data and how it is connected to other data. + +**Sequence**: A stateless grouping of actions that control how an Application responds to requests. + +**Service**: Operations implemented in an external system. + +**Repository**: A type of Service that represents a collection of data within a DataSource. For more information, see [Repositories](Repositories.md). diff --git a/docs/Implementing-features.md b/docs/Implementing-features.md new file mode 100644 index 000000000000..fd469095defb --- /dev/null +++ b/docs/Implementing-features.md @@ -0,0 +1,580 @@ +--- +lang: en +title: 'Implementing features' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Implementing-features.html +summary: +--- + +{% include previous.html content=" +This article continues off from [Defining your testing stategy(./Defining-your-testing-strategy.md$2). +" %} + +## Incrementally implement features + +To recapitulate the status of your new project: + + - You have defined your application's API and described it in an OpenAPI Spec document. + - You have empty controllers backing your new operations. + - Our project has a test verifying the validity of your API spec. This test passes. + - Our test suite includes a smoke test to verify conformance of your implementation + with the API spec. These checks are all failing now. + +Now it's time to put your testing strategy outlined in the previous section into practice. Pick one of the new API operations, preferably the one that's easiest to implement, and get to work! + +Start with `GET /product/{slug}` in this guide. + +### Write an acceptance test + +One "acceptance test", where you start the application, make an HTTP request to search for a given product name, and verify that expected products were returned. This verifies that all parts of your application are correctly wired together. + +Create `test/acceptance/product.acceptance.ts` with the following contents: + +```ts +import {HelloWorldApp} from '../..'; +import {RestBindings, RestServer} from '@loopback/rest'; +import {expect, supertest} from '@loopback/testlab'; + +describe('Product (acceptance)', () => { + let app: HelloWorldApp; + let request: supertest.SuperTest; + + before(givenEmptyDatabase); + before(givenRunningApp); + + it('retrieves product details', async () => { + // arrange + const product = await givenProduct({ + name: 'Ink Pen', + slug: 'ink-pen', + price: 1, + category: 'Stationery', + description: 'The ultimate ink-powered pen for daily writing', + label: 'popular', + available: true, + endDate: null, + }); + + // act + const response = await request.get('/product/ink-pen') + + // assert + expect(response.body).to.deepEqual({ + id: product.id, + name: 'Ink Pen', + slug: 'ink-pen', + price: 1, + category: 'Stationery', + available: true, + description: 'The ultimate ink-powered pen for daily writing', + label: 'popular', + endDate: null, + }); + }); + + async function givenEmptyDatabase() { + // TODO + } + + async function givenRunningApp() { + app = new HelloWorldApp(); + const server = await app.getServer(RestServer); + server.bind(RestBindings.PORT).to(0); + await app.start(); + + const port: number = await server.get(RestBindings.PORT); + request = supertest(`http://127.0.0.1:${port}`); + } + + async function givenProduct(data: Object) { + // TODO + return Object.assign({}, data, {id: 1}); + } +}); +``` + +Notice there are few missing pieces annotated with TODO comments. Well come back to them very soon. Remember, when practicing TDD in small steps, +the goal is to add as little test code as needed to uncover a missing piece in the production code, and then add just enough production code to +make your new test pass (while keeping all existing tests passing too). + +Run the tests and watch the new test fail: + +``` + 1) Product (acceptance) retrieves product details: + Error: expected 200 "OK", got 404 "Not Found" +``` + +When you scroll up in the test output, you will see more information about the 404 error: + +``` +Unhandled error in GET /product/ink-pen: 404 Error: Controller method not found: ProductController.getDetails +``` + +Learn more about acceptance testing in [Test your individual REST API endpoints](./Testing-your-application.md#test-your-individual-rest-api-endpoints) from [Testing your application](./Testing-your-application.md). + +### Write a unit-test for the new controller method + +The new acceptance test is failing because there is no `getDetails` method implemented by `ProductController`. Start with a unit-test to drive the implementation of this new method. Please refer to [Unit-test your Controllers](./Testing-your-application.md#unit-test-your-controllers) for more details. + +Create `tests/unit/product-controller.test.ts` with the following contents: + +```ts +import {ProductController} from '../..'; +import {expect} from '@loopback/testlab'; + +describe('ProductController', () => { + describe('getDetails()', () => { + it('retrieves details of a given product', async () => { + const controller = new ProductController(); + const details = controller.getDetails('ink-pen'); + expect(details).to.containDeep({ + name: 'Ink Pen', + slug: 'ink-pen', + }); + }); + }); +}); +``` + +This test is clearly not describing a final solution, for example there is no Product model and repository involved. However, it's a good first increment that drives enough of the initial controller implementation. +This shows the power of unit testing - you can test this new controller method in isolation, independent from the other moving parts of the +application, even before those other parts are implemented! + +Run `npm test` and watch the test fail with a helpful error message: + +``` +TSError: ⨯ Unable to compile TypeScript +test/unit/product-controller.test.ts (13,40): Property 'getDetails' does not exist on type 'ProductController'. (2339) +``` + +Now it's time to write the first implementation of the `getDetails` method. Modify the file `src/controllers/product-controller.ts` as follows: + +```ts +export class ProductController { + async getDetails(slug: string) { + return { + name: 'Ink Pen', + slug: 'ink-pen', + }; + } +} +``` + +Run `npm test` to see your new test pass. Notice that the Dredd-powered test of `/product/{slug}` is passing too, and your acceptance test is failing +with a different error now - the response does not contain all expected product properties. + +While it's possible to further iterate by adding more unit tests, a more valuable next step is to write an integration test to verify that your new API is using data from your backing database. + +### Write an integration test for the new controller method + +Create `tests/integration/product-controller.integration.ts` with the following contents: + +```ts +import {ProductController} from '../..'; +import {expect} from '@loopback/testlab'; + +describe('ProductController (integration)', () => { + beforeEach(givenEmptyDatabase); + + describe('getDetails()', () => { + it('retrieves details of the given product', async () => { + const inkPen = await givenProduct({ + name: 'Ink Pen', + slug: 'ink-pen', + }); + + const pencil = await givenProduct({ + name: 'Pencil', + slug: 'pencil', + }); + + const controller = new ProductController(); + + const details = await controller.getDetails('pencil'); + + expect(details).to.eql(pencil); + }); + }); + + async function givenEmptyDatabase() { + // TODO + } + + async function givenProduct(data: Object) { + // TODO + return Object.assign({}, data, {id: 1}); + } +}); +``` + +Run `npm test` to see the new test fail. + +``` +AssertionError: expected Object { name: 'Ink Pen', slug: 'ink-pen' } to equal Object { name: 'Pencil', slug: 'pencil', id: 1 } (at name, Ahas 'Ink Pen' and B has 'Pencil') ++ expected - actual + + { + - "name": "Ink Pen" + - "slug": "ink-pen" + + "id": 1 + + "name": "Pencil" + + "slug": "pencil" + } +``` + +Please refer to [Test your Controllers and Repositories together](./Testing-your-application.md#test-your-controllers-and-repositories-together) to learn more about integration testing. + +Take a closer look at the new test. To make it fail with the current implementation, you need to find a different scenario compared to what is covered by the unit test. You could simply change the data, but that would add little value to the test suite. Instead, take this opportunity to cover another requirement of "get product details" operation: it should return the details of the product that matches the "slug" parameter passed in the arguments. + +The next step is bigger than is usual in an incremental TDD workflow. You need to connect to the database and define classes to work with the data. + +### Define Product model, repository, and data source + +LoopBack is agnostic when it comes to accessing databases. You can choose any package from the npm module ecosystem. On the other hand, we also want LoopBack users to have a recommended solution that's covered by technical support. Welcome to `@loopback/repository`, a TypeScript facade for the `loopback-datasource-juggler` implementation in LoopBack 3.x. + + 1. Define `Product` model in `src/models/product.model.ts` + + ```ts + import {Entity, model, property} from '@loopback/repository'; + + @model() + export class Product extends Entity { + @property({ + description: 'The unique identifier for a product', + id: true, + }) + id: number; + + @property({required: true}) + name: string; + + @property({required: true}) + slug: string; + + @property({required: true}) + price: number; + + // Add the remaining properties yourself: + // description, available, category, label, endData + } + ``` + + **TODO(bajtos) Find out how to re-use ProductBaseSchema for the model definition** + + 2. Define a data source representing a single source of data, typically a database. This example uses in-memory storage. In real applications, replace the `memory` connector with the actual database connector (`postgresql`, `mongodb`, etc.). + + Create `src/datasources/db.datasource.ts` with the following content: + + ```ts + import {juggler, DataSourceConstructor} from '@loopback/repository'; + + export const db = new DataSourceConstructor({ + connector: 'memory', + }); + ``` + + 3. Define `ProductRepository` in `src/repositories/product.repository.ts` + + ```ts + import {DefaultCrudRepository, DataSourceType} from '@loopback/repository'; + import {Product} from '../models/product.model'; + import {db} from '../datasources/db.datasource'; + + export class ProductRepository extends DefaultCrudRepository< + Product, + typeof Product.prototype.id + > { + constructor() { + super(Product, db); + } + } + ``` + +See [Repositories](Repositories.md) for more details on this topic. + +### Update test helpers and the controller use real model and repository + +Rework `givenEmptyDatabase` and `givenProduct` as follows: + +```ts +async function givenEmptyDatabase() { + await new ProductRepository().deleteAll(); +} + +async function givenProduct(data: Partial) { + return await new ProductRepository().create(Object.assign({ + name: 'a-product-name', + slug: 'a-product-slug', + price: 1, + description: 'a-product-description', + available: true, + }, data)); +} +``` + +Notice that `givenProduct` is filling in required properties with sensible defaults. This is allow tests to provide only a small subset of properties that are strictly required by the tested scenario. This is important for multiple reasons: + + 1. It makes tests easier to understand, because it's immediately clear what model properties are relevant to the test. If the test was setting all required properties, it would be difficult to tell whether some of those properties may be actually relevant to the tested scenario. + + 2. It makes tests easier to maintain. As the data model evolves, you will eventually need to add more required properties. If the tests were building product instances manually, you would have to fix all tests to set the new required property. With a shared helper, there is only a single place where to add a value for the new required property. + +You can learn more about test data builders in [Use test data builders](./Testing-your-application.md#use-test-data-builders) section of [Testing your application](./Testing-your-application.md). + +Now that the tests are setting up the test data correctly, it's time to rework `ProductController` to make the tests pass again. + +```ts +import {ProductRepository} from '../repositories/product.repository'; +import {Product} from '../models/product.model'; + +export class ProductController { + repository: ProductRepository = new ProductRepository(); + + async getDetails(slug: string): Promise { + const found = await this.repository.find({where: {slug}}); + // TODO: handle "not found" case + return found[0]; + } +} +``` + +### Run tests + +Run the tests again. These results may surprise you: + + 1. The acceptance test is failing: the response contains some expected properties (slug, name), + but is missing most of other properties. + + 2. The API smoke test is failing with a cryptic error. + + 3. The unit test is passing, despite the fact that it's not setting up any data at all! + +Examine the acceptance test first. A quick review of the source code should tell us what's the problem - the test is relying on `givenEmptyDatabase` and `givenProduct` helpers, but these helpers are not fully implemented yet. Fix that by reusing the helpers from the integration test: Move the helpers to `test/helpers/database.helpers.ts` and update both the acceptance and the integration tests to import the helpers from there. + +To find out why the API smoke test is failing, you can start the application via `node .` and request the tested endpoint for example using `curl`. You will see that the server responds with 200 OK and an empty response body. This is because `getDetails` returns `undefined` when no product matching the slug was found. This can be fixed by adding a `before` hook to the smoke test. The hook should populate the database with the test data assumed by the examples in the Swagger specification by leveraging existing helpers `givenEmptyDatabase` and `givenProduct`. + +Now back to the first unit test. It may be a puzzle to figure out why the test is passing, although the answer is simple: the integration and acceptance tests are setting up data in the database, the unit test does not clear the database (because it should not use it at all!) and accidentally happen +to expect the same data as one of the other tests. + +### Decouple Controller from Repository + +This shows us a flaw in the current design of the `ProductController` - it's difficult to test it in isolation. Fix that by making the dependency on `ModelRepository` explicit and allow controller users to provide a custom implementation. + +```ts +class ProductController { + constructor(public repository: ProductRepository) {} + + // ... +} +``` + +{% include tip.html content="If you wanted to follow pure test-driven development, then you would define a minimal repository interface describing only the methods actually used by the controller. This will allow you to discover the best repository API that serves the need of the controller. However, you don't want to design a new repository API, you want to re-use the repository implementation provided by LoopBack. Therefore using the actual Repository class/interface is the right approach. +" %} + +In traditional object-oriented languages like Java or C#, in order to allow the unit tests to provide a custom implementation of the repository API, the controller needs to depend on an interface describing the API, and the repository implementation needs to implement this interface. The situation is easier in JavaScript and TypeScript. Thanks to the dynamic nature of the language, it's possible to mock/stub entire classes. The [sinon](http://sinonjs.org/) testing module, which comes bundled in `@loopback/testlab`, makes this very easy. + +Here is the updated unit test leveraging dependency injection: + +```ts +import {ProductController, ProductRepository} from '../..'; +import {expect, sinon} from '@loopback/testlab'; + +describe('ProductController', () => { + let repository: ProductRepository; + beforeEach(givenStubbedRepository); + + describe('getDetails()', () => { + it('retrieves details of a product', async () => { + const controller = new ProductController(repository); + const findStub = repository.find as sinon.SinonStub; + findStub.resolves([ + { + id: 1, + name: 'Ink Pen', + slug: 'ink-pen', + }, + ]); + + const details = await controller.getDetails('ink-pen'); + + expect(details).to.containDeep({ + name: 'Ink Pen', + slug: 'ink-pen', + }); + expect(findStub).to.be.calledWithMatch({where: {slug: 'ink-pen'}}); + }); + }); + + function givenStubbedRepository() { + repository = sinon.createStubInstance(ProductRepository); + } +}); +``` + +The new unit test is passing now, but the integration and acceptance tests are broken again! + + 1. Fix the integration test by changing how the controller is created - inject `new ProductRepository()` into the repository argument. + + 2. Fix the acceptance test by annotating `ProductController`'s `repository` argument with `@inject('repositories.Product')` + and binding the `ProductRepository` in the main application file where you are also binding controllers. + +Learn more about this topic in [Unit-test your Controllers](./Testing-your-application.md#unit-test-your-controllers) +and [Use test doubles](./Testing-your-application.md#use-test-doubles) from [Testing your application](./Testing-your-application.md). + +### Handle 'product not found' error + +When you wrote the first implementation of `getDetails`, you assumed the slug always refer to an existing product, which obviously is not always true. Fix the controller to correctly handle this error situation. + +Start with a failing unit test: + +```ts +it('returns 404 Not Found for unknown slug', async () => { + const controller = new ProductController(repository); + const findStub = repository.find as sinon.SinonStub; + findStub.resolves([]); + + try { + await controller.getDetails('unknown-slug'); + throw new Error('getDetails should have thrown and error'); + } catch (err) { + expect(err).to.have.property('statusCode', 404); + expect(err.message).to.match(/not found/i); + } +}); +``` + +Then fix `getDetails` implementation: + +```ts +// ... +import {HttpErrors} from '@loopback/rest'; + +export class ProductController { + // ... + + async getDetails(slug: string): Promise { + const found = await this.repository.find({where: {slug}}); + if (!found.length) { + throw new HttpErrors.NotFound(`Slug not found: ${slug}`); + } + return found[0]; + } +} +``` + +More information on `HttpErrors` can be found in [Controllers](./Controllers.md#handling-errors-in-controllers) + +### Implement a custom Sequence + +LoopBack 3.x is using Express middleware to customize the sequence of actions executed to handle an incoming request: body-parser middleware is converting the request body from JSON to a JavaScript object, strong-error-handler is creating an error response when the request failed. + +Express middleware has several shortcomings: + - It's based on callback flow control and does not support async functions returning Promises. + - The order in which middleware needs to be registered can be confusing, for example request logging middleware must be registered as the first one, despite the fact that the log is written only at the end, once the response has been sent. + - The invocation of middleware handlers is controlled by the framework, application developers have very little choices. + +LoopBack 4, abandons Express/Koa-like middleware for a different approach that puts the application developer in the front seat. See [Sequence](Sequence.md) documentation to learn more about this concept. + +Now you are going to modify request handling in the application to print a line in the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format) for each request handled. + +Start by writing an acceptance test, as described in [Test sequence customizations](Testing-your-application.md#test-sequence-customizations) from [Testing your application](Testing-your-application.md). Create a new test file (e.g. `sequence.acceptance.ts`) and add the following test: + +```ts +describe('Sequence (acceptance)', () => { + let app: HelloWorldApp; + let request: Client; + + before(givenEmptyDatabase); + beforeEach(givenRunningApp); + + it('prints a log line for each incoming request', async () => { + const logs: string[] = []; + const server = await app.getServer(RestServer); + server.bind('sequence.actions.commonLog').to((msg: string) => logs.push(msg)); + + const product = await givenProduct({name: 'Pen', slug: 'pen'}); + await request.get('/product/pen'); + expect(logs).to.have.length(1); + expect(logs[0]).to.match( + /^(::ffff:)?127\.0\.0\.1 - - \[[^]+\] "GET \/product\/pen HTTP\/1.1" 200 -$/, + ); + }); +}); +``` + +Run the test suite and watch the test fail. + +In the next step, copy the default Sequence implementation to a new project file `src/server/sequence.ts`: + +```ts +const RestSequenceActions = RestBindings.SequenceActions; + +export class MySequence implements SequenceHandler { + constructor( + @inject(RestSequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(RestSequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(RestSequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(RestSequenceActions.SEND) protected send: Send, + @inject(RestSequenceActions.REJECT) protected reject: Reject, + ) {} + + async handle(req: ParsedRequest, res: ServerResponse) { + try { + const route = this.findRoute(req); + const args = await this.parseParams(req, route); + const result = await this.invoke(route, args); + this.send(res, result); + } catch (err) { + this.reject(res, req, err); + } + } +} +``` + +Register your new sequence with your `Server`, for example by calling `server.sequence(MySequence)`. Run your tests to verify that everything works the same way as before and the new acceptance test is still failing. + +Now it's time to customize the default sequence to print a common log line. Edit the `handle` method as follows: + +```ts +async handle(req: ParsedRequest, res: ServerResponse) { + try { + const route = this.findRoute(req); + const args = await this.parseParams(req, route); + const result = await this.invoke(route, args); + this.send(res, result); + this.log([ + req.socket.remoteAddress, + '-', + '-', + `[${strftime('%d/%b/%Y:%H:%M:%S %z', new Date())}]`, + `"${req.method} ${req.path} HTTP/${req.httpVersion}"`, + res.statusCode, + '-', + ].join(' ')); + } catch (err) { + this.reject(res, req, err); + } +} +``` + +To inject the new method `log`, add the following line to `MySequence` constructor arguments: + +```ts +@inject('sequence.actions.log') protected log: (msg: string) => void +``` + +When you run the tests now, you will see that the new acceptance tests for logging passes, but some of the older acceptance tests started to fail. This is because `sequence.actions.log` is not bound in the application. Fix that by adding the following line after you've retrieved your rest server instance: + +```ts +// assuming you've called `const server = await app.getServer(RestServer)` +server.bind('sequence.actions.log').to((msg: String) => console.log(msg)); +``` + +With this last change in place, your test suite should be all green again. + +The next task is left as an exercise for the reader: \Modify the `catch` block to print a common log entry too. Start by writing a unit-test that invokes `MySequence` directly. + +{% include next.html content= " +[Preparing the API for consumption](./Preparing-the-API-for-consumption.md) +" %} diff --git a/docs/Introduction-to-LoopBack-Next-development.md b/docs/Introduction-to-LoopBack-Next-development.md new file mode 100644 index 000000000000..bae90ae92d73 --- /dev/null +++ b/docs/Introduction-to-LoopBack-Next-development.md @@ -0,0 +1,12 @@ +--- +lang: en +title: 'Introduction to LoopBack 4 development' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Intro-to-LB4-development.html +summary: +--- +MOVED TO strongloop.com. + +Working draft: https://github.com/strongloop-forks/strongloop.com/blob/master/_posts/2017-10-15-intro-to-lb4-for-developers.md diff --git a/docs/Language-related-concepts.md b/docs/Language-related-concepts.md new file mode 100644 index 000000000000..5d7147de2da2 --- /dev/null +++ b/docs/Language-related-concepts.md @@ -0,0 +1,13 @@ +--- +lang: en +title: 'Language-related concepts' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Language-related-concepts.html +summary: +--- + +A module that exports JavaScript/TypeScript concept related functions. + +- [**Mixin**](Mixin.md): Add properties and methods to a class. diff --git a/docs/LoopBack-3.x.md b/docs/LoopBack-3.x.md new file mode 100644 index 000000000000..b48c59426f95 --- /dev/null +++ b/docs/LoopBack-3.x.md @@ -0,0 +1,116 @@ +--- +lang: en +title: 'For current LoopBack users' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/LoopBack-3.x.html +summary: +--- + +LoopBack 4 is the next generation of the LoopBack framework, with a completely rewritten core foundation and significantly improved programming model. If you're an existing LoopBack user, read [Crafting LoopBack 4](Crafting-LoopBack-4.html) to understand the motivations, strategy, and innovations behind this exciting new version. + +This article will help existing users understand LoopBack 4: + +- How to connect LoopBack 3.x concepts to LoopBack 4 terms +- What takes to rewrite/migrate a LoopBack 3.x application to LoopBack 4 +- What's new and exciting in LoopBack 4 +- What's available in the beta release +- What's on the roadmap to achieve functional parity +- What you can try with the beta release + +## Overview + +At high-level, LoopBack 3.x applications consist of three big "parts" + + - Persistence layer (this includes talking to backend services like SOAP/REST) + - Outwards facing REST API + - App-wide setup - Express middleware, boot scripts, etc. + +In the persistence layer, users can contribute the following artifacts: + + 1. Definitions of Model data types (properties, validations) + 2. Definition of data sources + 3. Configuration of models (which datasource are they attached to) + 4. Operation hooks + +At the public API side, users can define: + + 1. Which built-in methods should be exposed (think of `disableRemoteMethodByName`) + 1. Custom remote methods + 2. before/after/afterError hooks at application-level + 3. before/after/afterError hooks at model-level + 4. before/after/afterError hooks at model method level + +LoopBack Next was intentionally designed to allow users to choose their ORM/persistence solution, and our initial version of @loopback/repository is based on juggler 3.x. That makes it possible for users to reuse their existing model definitions, migrating their application incrementally. + +## Concept/feature mapping + +In Loopback 3.x (and earlier), models were responsible for both accessing data in other systems (databases, SOAP services, etc.) and providing the application's external REST API. This made it easy to quickly build a REST interface for an existing database, but difficult to customize the REST API and fine-tune it to the needs of application clients. + +LoopBack v4 is moving to the well-known Model-(View-)Controller pattern, where the code responsible for data access and manipulation is separated from the code responsible for implementing the REST API. + +[loopback-next-example](https://github.com/strongloop/loopback-next-example) demonstrates this loose coupling. Facade is the top-level service that serves the account summary API, and is dependent on the three services Account, Customer, and Transaction. But the facade only aggregates the calls to the three services, and is not tightly coupled with the service implementation; that's why it is independent of the three services. We can define the APIs in facade the way we want. Thus, code responsible for data access and manipulation is separated from the code responsible for implementing client side APIs. + + +| Concept/Feature | LoopBack 3.x | LoopBack 4 | +| --------------------- | ---------------------------------------------- | ------------------------------------------------- | +| Programming Language | Built with JavaScript ES5
Node.js callback | TypeScript 2.5.x & JavaScript ES2016/2017
Promise & Async/Await | +| Core foundation | Express with LoopBack extensions | Home-grown IoC container | +| Model Definition | Models can be defined with JavaScript or json | Models can be defined with TypeScript/JavaScript/JSON | +| Model Persistence | A model can be attached to a datasource backed by a connector that implements CRUD operations | Repository APIs are introduced to represent persistence related operations. Repository is the binding of model metadata to a datasource | +| Model Relation | Relations can be defined between models | (TBA) Relations can be defined between models but they will be realized between repositories | +| Model Remoting | JavaScript/JSON remoting metadata is used to describe method signatures and their mapping to REST/HTTP
Swagger specs are generated after the fact | Remoting metadata can be supplied by OpenAPI JSON/YAML documents or TypeScript decorators | +| API Spec | Swagger 2.0 | Swagger 2.0 and OpenAPI Spec 3.0, potentially other forms such as gRPC or GraphQL | +| API Explorer | Built-in UI based on swagger-ui (/explorer) | (Beta) Expose Swagger/OpenAPI specs and a browser redirect to editor.swagger.io | +| DataSource | JSON and JS | Same as 3.x | +| Connector | Plain JS | JS and TypeScript | +| Mixin | Use a utility to add methods from the mixin to the target model class | Use ES2015 mixin classes pattern supported by [TypeScript 2.2 and above](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html) | +| Middleware | Express middleware with phase-based registration and ordering | Sequence consists of actions | +| Remote hooks | Before/After hooks for remote methods | Controller-level sequence/actions | +| Boot script | Scripts to be invoked during bootstrapping | (TBD) | +| CRUD operation hooks | Hooks for CRUD operations | | +| Built-in models | Built-in User/AccessToken/Application/ACL/Role/RoleMapping for AAA | (TBD) | +| Authentication | User model as the login provider
loopback-component-passport | Authentication component with extensibility to strategy providers | +| Authorization | Use built-in User/Application/AccessToken model for identity and ACL/Role/RoleMapping for authorization | Authorization component | +| Component | A very simple implementation to configure and invoke other modules | A fully-fledged packaging model that allows contribution of extensions from other modules | +| Tooling | loopback-cli and API Connect UI | (TBA) | + + +## What's new and exciting in LoopBack 4 + +Some of the highlights of LoopBack 4 include: + +- Leverage TypeScript for better code quality and productivity +- Unify and simplify the asynchronous programming model/style around Promise and Async/Await +- Implement an IoC Container with Dependency Injection for better visibility, extensibility and composability +- Introduce Component as packaging model for extensions that can be plugged into LoopBack 4 applications +- Make everything else as components, such as REST, Authentication, and Authorization +- Divide the responsibilities of LoopBack models into + - Controllers - to handle incoming API requests + - Repositories - to provide access to data stores + - Models - to define schemas for business objects + - Services - to interact with existing REST APIs, SOAP WebServices, and other form of services/microservices +- Refactor the ORM into separate modules for different concerns + +## What's in the beta release + +The beta release is the first milestone of the LoopBack 4 journey. Although it's not functionally complete or ready for production use, it provides a preview of what's coming, including: + +1. A new `@loopback/context` module that implements an IoC container with dependency injection +2. A new `@loopback/core` module that defines core artifacts such as application and component +3. A `@loopback/rest` component that provides top-down REST API mapping using OpenAPI/Swagger specs and controllers +4. A `@loopback/authentication` component to provide infrastructure to integrate with authentication providers +5. An experimental `@loopback/repository` module to define repository interfaces and provide a reference implementation on top of legacy `loopback-datasource-juggler` and connectors +6. Examples and tutorials + +The primary target audience of the beta release is extension developers. Please check out https://github.com/strongloop/loopback4-example-log-extension. + +The initial beta release provides a preview for API developers. Currently, the LoopBack CLI doesn't yet support LoopBack 4, but it will eventually. See a working application at https://github.com/strongloop/loopback-next-hello-world. + +## Tentative roadmap + +> Disclaimer: The release plan is tentative and it's subject to changes as the core team and community contributors make progress incrementally. + +- https://github.com/strongloop/loopback-next/wiki/Upcoming-Releases + diff --git a/docs/Mixin.md b/docs/Mixin.md new file mode 100644 index 000000000000..aafe87b1f585 --- /dev/null +++ b/docs/Mixin.md @@ -0,0 +1,124 @@ +--- +lang: en +title: 'Mixin' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Mixin.html +summary: +--- + +It is a commonly used JavaScript/TypeScript strategy to extend a class with new properties and methods. + +A good approach to apply mixins is defining them as sub-class factories. +Then declare the new mixed class as: + +```js +class MixedClass extends MixinFoo(MixinBar(BaseClass)) {}; +``` + +Check article [real mixins with javascript classes](http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/) +to learn more about it. + +## Define Mixin + +By defining a mixin, you create a mixin function that takes in a base class, +and returns a new class extending the base class with new properties and methods mixed to it. + +For example you have a simple controller which only has a greeter function prints out 'hi!': + +{% include code-caption.html content="Controllers/myController.ts" %} + +```ts +class SimpleController { + constructor() { + + } + greet() { + console.log('hi!'); + } +} +``` + +Now let's add mixins to it: + +- A time stamp mixin that adds a property `createdAt` to a record when a +controller instance is created. + +- A logger mixin to provide logging tools. + +Define mixin `timeStampMixin`: + +{% include code-caption.html content="Mixins/timeStampMixin.ts" %} + +```ts +import {Class} from "@loopback/repository"; + +export function timeStampMixin> (baseClass: T) { + return class extends baseClass { + // add a new property `createdAt` + public createdAt: Date; + constructor(...args: any[]) { + super(args); + this.createTS = new Date(); + } + printTimeStamp() { + console.log('Instance created at: ' + this.createdAt); + } + } +} +``` + +And define mixin `loggerMixin`: + +{% include code-caption.html content="Mixins/loggerMixin.ts" %} + +```ts +import {Class} from "@loopback/repository"; + +function loggerMixin> (baseClass: T) { + return class extends baseClass { + // add a new method `log()` + log(str: string) { + console.log('Prints out a string: ' + str); + }; + } +} +``` + +Now you can extend `SimpleController` with the two mixins: + +{% include code-caption.html content="Controllers/myController.ts" %} + +```ts +import {timeStampMixin} from 'Mixins/timeStampMixin.ts'; +import {loggerMixin} from 'Mixins/loggerMixin.ts'; + +class SimpleController { + constructor() { + + } + greet() { + console.log('hi!'); + } +} + +class AdvancedController extends loggerMixin(timeStampMixin(SimpleController)) {}; + +// verify new method and property are added to `AdvancedController`: +let aControllerInst = new AdvancedController(); +aControllerInst.printTimeStamp(); +// print out: Instance created at: Tue Oct 17 2017 22:28:49 GMT-0400 (EDT) +aControllerInst.logger('hello world!'); +// print out: Prints out a string: hello world! +``` + +## References + +Here are some articles explaining ES2015 and TypeScript mixins in more details: + +- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Mix-ins + +- http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/ + +- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html \ No newline at end of file diff --git a/docs/Model.md b/docs/Model.md new file mode 100644 index 000000000000..052c6ebf665b --- /dev/null +++ b/docs/Model.md @@ -0,0 +1,268 @@ +--- +lang: en +title: 'Model' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Model.html +summary: +--- +{% include content/tbd.html %} + +## Overview +A `model` represents the definition of a model in LoopBack, with respect to +the [datasource juggler](https://github.com/strongloop/loopback-datasource-juggler). +Currently, we provide the `@loopback/repository` module, which provides special +decorators for adding metadata to your TypeScript/JavaScript +classes in order to use them with the legacy implementation of the Juggler. + +## Definition of a Model +At its core, a model in LoopBack is a simple JavaScript class. + +```ts +export class Customer { + email: string; + isMember: boolean; + cart: ShoppingCart; +} +``` + +Extensibility is a core feature of LoopBack. There are external packages that +add additional features, for example, integration with the legacy juggler or +JSON Schema generation. These features become available to a LoopBack model +through the ~~usage of~~ `@model` and `@property` decorators from the +`@loopback/repository` module. + +```ts +import {model, property} from '@loopback/repository'; + +@model() +export class Customer { + @property() email: string; + @property() isMember: boolean; + @property() cart: ShoppingCart; +} +``` + +## Using Legacy Juggler +To define a model for use with the legacy juggler, extend your classes from +`Entity` and decorate them with the `@model` and `@property` decorators. + +```ts +import {model, property} from '@loopback/repository'; + +@model() +export class Product extends Entity { + @property({ + id: true, + description: 'The unique identifier for a product', + }) + id: number; + + @property() + name: string; + + @property() + slug: string; + + constructor(data?: Partial) { + super(data); + } +} +``` + +### Model Decorator +The model decorator can be used without any additional parameters, or can be +passed in a + +[ModelDefinitionSyntax](https://loopback.io/doc/en/lb3/Model-definition-JSON-file.html) +object which follows the general format provided in LoopBack 3: +```ts +@model({ + name: "Category", + properties: { + // define properties here. + }, + settings: { + // etc... + } +}) +class Category extends Entity { + // etc... +} +``` + +However, the model decorator already knows the name of your model class, so you +can omit it. +```ts +@model() +class Product extends Entity { + name: string; + // other properties... +} +``` + +Additionally, the model decorator is able to build the properties object through +the information passed in or inferred by the property decorators, so the +properties key value pair can be omitted as well by using property decorators. + +### Property Decorator +The property decorator takes in the same arguments used in LoopBack 3 for +individual property entries: +```ts +@model() +class Product extends Entity { + @property({ + name: "name", + description: "The product's common name.", + type: "string", + }) + public name: string; +} +``` + +The complete list of valid attributes for property definitions can be found in +LoopBack 3's [Model definition section](../lb3/Model-definition-JSON-file.md#properties) + + +The property decorator leverages LoopBack's [metadata package](https://github.com/strongloop/loopback-next/tree/master/packages/metadata) +to determine the type of a particular property. + +```ts +@model() +class Product extends Entity { + @property() + public name: string; // The type information for this property is String. +} +``` + +### Array Property Decorator + +There is a limitation to the metadata that can be automatically inferred by +LoopBack, due to the nature of arrays in JavaScript. In JavaScript, arrays do +not possess any information about the types of their members. By traversing an +array, you can inspect the members of an array to determine if they are of a +primitive type (string, number, array, boolean), object or function, but this +would not tell us anything about what the value would be if it were an object or +function. + +For consistency, we require the use of the `@property.array` +decorator, which adds the appropriate metadata for type inference of your array +properties. + +```ts +@model() +class Order extends Entity { + @property.array(Product) items: Product[]; +} + +@model() +class Thread extends Entity { + // Note that we still require it, even for primitive types! + @property.array(String) posts: string[]; +} +``` + +Additionally, the `@property.array` decorator can still take an optional 2nd +parameter to define or override metadata in the same fashion as the `@property` +decorator. +```ts +@model() +class Customer extends Entity { + @property.array(String, { + name: 'names', + required: true, + }) aliases: string[]; +} +``` + +### JSON Schema inference +Use the `@loopback/repository-json-schema module` to build a JSON schema from +a decorated model. Type information is inferred from the `@model` and +`@property` decorators. The `@loopback/repository-json-schema` module contains +the `getJsonSchema` function to access the metadata stored by the decorators +to build a matching JSON Schema of your model. + +```ts +import {model, property} from '@loopback/repository'; +import {getJsonSchema} from '@loopback/repository-json-schema'; + +@model() +class Category { + @property() name: string; +} + +@model() +class Product { + @property({required: true}) name: string; + @property() type: Category; +} + +const jsonSchema = getJsonSchema(Product); +``` + +`jsonSchema` from above would return: + +```json +{ + "title": "Product", + "properties": { + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/Category" + } + }, + "definitions": { + "Category": { + "properties": { + "name": { + "type": "string" + } + } + } + }, + "required": ["name"] +} +``` + +If a custom type is specified for a decorated property in a model definition, +then a reference [`$ref`](http://json-schema.org/latest/json-schema-core.html#rfc.section.8) +field is created for it and a `definitions` sub-schema is created at the +top-level of the schema. The `definitions` sub-schema is populated with the type +definition by recursively calling `getJsonSchema` to build its properties. +This allows for complex and nested custom type definition building. +The example above illustrates this point by having the custom type `Category` +as a property of our `Product` model definition. + +#### Supported JSON keywords + +{% include note.html content=" + +This feature is still a work in progress and is incomplete. + +" %} + +Following are the supported keywords that can be explicitly passed into the decorators +to better tailor towards the JSON Schema being produced. + +| Keywords | Decorator | Type | Default | Description | +|-------------|-------------|---------|--------------|---------------------------------------------------------| +| title | `@model` | string | *model name* | Name of the model | +| description | `@model` | string | | Description of the model | +| array | `@property` | boolean | | Used to specify whether the property is an array or not | +| required | `@property` | boolean | | Used to specify whether the property is required or not | + +## Other ORMs +You might decide to use an alternative ORM/ODM in your LoopBack application. +Loopback v4 no longer expects you to provide your data in its own custom Model +format for routing purposes, which means you are free to alter your classes +to suit these ORMs/ODMs. + +However, this also means that the provided schema decorators will serve no +purpose for these ORMs/ODMs. Some of these frameworks may also provide +decorators with conflicting names (ex. another `@model` decorator), which might +warrant avoiding the provided juggler decorators. + diff --git a/docs/Preparing-the-API-for-consumption.md b/docs/Preparing-the-API-for-consumption.md new file mode 100644 index 000000000000..e1f22bcb6217 --- /dev/null +++ b/docs/Preparing-the-API-for-consumption.md @@ -0,0 +1,88 @@ +--- +lang: en +title: 'Preparing the API for consumption' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Preparing-the-API-for-consumption.html +summary: +--- + +{% include previous.html content=" +This article continues off from [Implementing features](./Implementing-features.md). +" %} + +## Preparing your API for consumption + +### Interacting with your API + +We'll use the [@loopback/example-getting-started](https://github.com/strongloop/loopback-next/tree/master/packages/example-getting-started) +package to demonstrate how Swagger UI can be used to test your endpoints. + +First, use the [@loopback/cli tooling](https://github.com/strongloop/loopback-next/tree/master/packages/cli) +to install the example-getting-started, and then run the application: + +``` +$ npm i -g @loopback/cli +$ lb4 example +? What example would you like to clone? (Use arrow keys) +❯ getting-started: An application and tutorial on how to build with LoopBack 4. + hello-world: A simple hello-world Application using LoopBack 4 + log-extension: An example extension project for LoopBack 4 + rpc-server: A basic RPC server using a made-up protocol +$ cd loopback4-example-getting-started +$ npm i +$ npm start +``` + +Open [http://localhost:3000/swagger-ui](http://localhost:3000/swagger-ui) to see the API endpoints defined by `swagger.json`. + +{% include note.html content=" + Swagger UI provides users with interactive environment to test the API endpoints defined by the raw spec found at [http://localhost:3000/swagger.json](http://localhost:3000/swagger.json). + The API spec is also available in YAML flavour at [http://localhost:3000/swagger.yaml](http://localhost:3000/swagger.yaml) +" %} + +{% include image.html file="lb4/10000000.png" alt="" %} + +The Swagger UI displays all of the endpoints defined in your application. + +{% include image.html file="lb4/10000001.png" alt="" %} + +Clicking on one of the endpoints will show the endpoint's documentation as defined in your API spec. Next, click on `Try It Out` to send a request to the endpoint. If the endpoint takes parameters, assign the values before the request is sent. If the parameter involves a body, a template is given for you to edit as specified in your spec. Click `Execute` to send the request: + +{% include image.html file="lb4/10000002.png" alt="" %} + +The response to the request can be seen below the `Execute` button, where the response code and the body are displayed. Ideally, each endpoint should be tested with good and bad inputs to confirm that the returned responses are as expected. + +## Closing thoughts + +Congratulations! You now have successfully created and tested an API with LoopBack 4. We hope you enjoy the test-drive. Your feedback matters and please share your thoughts with us. + +This is just the beginning of the full LoopBack 4 developer experience. The first beta release lays out the new foundation of LoopBack for extension developers. It also demonstrates a path to create REST APIs from OpenAPI specs together with Controllers and Repositories. More features will be added in the coming weeks and months. + +Here is a sneak peek of what's coming: + +- More extensions and extension points an: [loopback-next issue #512](https://github.com/strongloop/loopback-next/issues/512) + +- Authorization component: [loopback-next issue #538](https://github.com/strongloop/loopback-next/issues/538) + +- Fully-fledged API explorer: [loopback-next issue #559](https://github.com/strongloop/loopback-next/issues/559) + +- Complete repository/service story for backend interactions + - [loopback-next issue #419](https://github.com/strongloop/loopback-next/issues/419) + - [loopback-next issue #537](https://github.com/strongloop/loopback-next/issues/537) + - [loopback-next issue #522](https://github.com/strongloop/loopback-next/issues/522) + +- Declarative support for various constructs + - [loopback-next issue #441](https://github.com/strongloop/loopback-next/issues/441) + - [loopback-next issue #461](https://github.com/strongloop/loopback-next/issues/461) + +- Alignment of microservices and cloud native experience + - [loopback-next issue #442](https://github.com/strongloop/loopback-next/issues/442) + - [loopback-next issue #25](https://github.com/strongloop/loopback-next/issues/25) + +- Tooling: [loopback-next issue #361](https://github.com/strongloop/loopback-next/issues/361) + +- Plain JavaScript: [loopback-next issue #560](https://github.com/strongloop/loopback-next/issues/560) + +The train is moving and welcome on board! Your participation and contribution will make LoopBack 4 an even more powerful framework and greater community/ecosystem. The team is very excited about the new journey. We look forward to working with you on more ideas, more pull requests, and more extension modules. Let's make LoopBack 4 rock together! diff --git a/docs/Reference.md b/docs/Reference.md new file mode 100644 index 000000000000..d056bb6e73b9 --- /dev/null +++ b/docs/Reference.md @@ -0,0 +1,11 @@ +--- +lang: en +title: 'Reference' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Reference.html +summary: +--- + +{% include list-children.html in=site.data.sidebars.lb4_sidebar %} diff --git a/docs/Repositories.md b/docs/Repositories.md new file mode 100644 index 000000000000..bbc0ce16c61b --- /dev/null +++ b/docs/Repositories.md @@ -0,0 +1,352 @@ +--- +lang: en +title: 'Repositories' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Repositories.html +summary: +--- +A Repository is a type of _Service_ that represents a collection of data within a DataSource. + + + +## Example Application +You can look at [the account application as an example.](https://github.com/strongloop/loopback-next-example/tree/master/services/account) + +## Installation +Legacy juggler support has been enabled in `loopback-next` and can be imported from the `@loopback/repository` package. In order to do this, save `@loopback/repository` as a dependency in your application. + +You can then install your favorite connector by saving it as part of your application dependencies. + +## Repository Mixin +`@loopback/repository` provides a mixin for your Application that enables convenience methods that automatically bind repository classes for you. Repositories declared by components are also bound automatically. + +Repositories are bound to `repositories.${ClassName}`. See example below for usage. +```ts +import { Application } from '@loopback/core'; +import { RepositoryMixin } from '@loopback/repository'; +import { AccountRepository, CategoryRepository } from './repository'; + +// Using the Mixin +class MyApplication extends RepositoryMixin(Application) {} + + +const app = new MyApplication(); +// AccountRepository will be bound to key `repositories.AccountRepository` +app.repository(AccountRepository); +// CategoryRepository will be bound to key `repositories.CategoryRepository` +app.repository(CategoryRepository); +``` + +## Configure datasources + +You can define a DataSource using legacy Juggler in your LoopBack 4 app as follows: + +```js +import {juggler, DataSourceConstructor} from '@loopback/repository'; + +export const db = new DataSourceConstructor({ + connector: 'mysql', + host: 'localhost', + port: 3306, + database: 'test', + password: 'pass', + user: 'root', +}); +``` + +## Define models + +Models are defined as regular JavaScript classes. If you want your model to be persisted in a database, your model must have an `id` property and inherit from `Entity` base class. + +TypeScript version: + +```ts +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Account extends Entity { + @property({id: true}) + id: number; + + @property({required: true}) + name: string; +} +``` + +JavaScript version: + +```js +import { + Entity, + model, + property, + ModelDefinition +} from '@loopback/repository'; + +export class Account extends Entity { + static definition = new ModelDefinition({ + name: 'Account', + properties: { + id: {type: 'number', id: true}, + name: {type: 'string', required: true}, + } + }); +} +``` + +## Define repositories + +Use `DefaultCrudRepository` class to create a repository leveraging the legacy juggler bridge and binding your Entity-based class with a datasource you have configured earlier. + +TypeScript version: + +```ts +import {DefaultCrudRepository} from '@loopback/repository'; +import {Account} from '../models/account.model'; +import {db} from '../datasources/db.datasource'; + +export class AccountRepository extends DefaultCrudRepository< + Account, + typeof Account.prototype.id +> { + constructor() { + super(Account, db); + } +} +``` + +JavaScript version: + +```ts +import {DefaultCrudRepository} from '@loopback/repository'; +import {Account} from '../models/account.model'; +import {db} from '../datasources/db.datasource'; + +export class AccountRepository extends DefaultCrudRepository { + constructor() { + super(Account, db); + } +} +``` + +### Controller Configuration + +Once your DataSource is defined for your repository, all the CRUD methods you call in your repository will use Juggler and your connector's methods unless you overwrite them. In your controller, you will need to define a repository property and create a new instance of the repository you configured your DataSource for in the constructor of your controller class as follows: + +```js +export class AccountController { + constructor( + @inject('repositories.account') + public repository: AccountRepository + ) {} +} +``` + +### Defining CRUD methods for your application + +When you want to define new CRUD methods for your application, you will need to modify the API Definitions and their corresponding methods in your controller. Here are examples of some basic CRUD methods: +1. Create API Definition: +```javascript +'/accounts/create': { + post: { + 'x-operation-name': 'createAccount', + parameters: [ + { + name: 'accountInstance', + in: 'body', + description: 'The account instance to create.', + required: true, + type: 'object' + }, + ], + responses: { + 200: { + schema: { + accountInstance: "#/definitions/Account" + }, + }, + }, + }, +} +``` +Create Controller method: +```javascript +async createAccount(accountInstance) { + return await this.repository.create(accountInstance); +} +``` +2. Find API Definition: +```javascript +'/accounts': { + get: { + 'x-operation-name': 'getAccount', + parameters: [ + { + name: 'filter', + in: 'query', + description: 'The criteria used to narrow down the number of accounts returned.', + required: false, + type: 'object' + } + ], + responses: { + 200: { + schema: { + type: 'array', + items: '#/definitions/Account' + }, + }, + }, + }, +} +``` +Find Controller method: +```javascript +async getAccount(filter) { + return await this.repository.find(JSON.parse(filter)); +} +``` +3. Update API Definition: +```javascript +'/accounts/update': { + post: { + 'x-operation-name': 'updateAccount', + parameters: [ + { + name: 'where', + in: 'query', + description: 'The criteria used to narrow down the number of accounts returned.', + required: true, + type: 'object' + }, + { + name: 'data', + in: 'body', + description: 'An object of model property name/value pairs', + required: true, + type: 'object' + } + ], + responses: { + 200: { + schema: { + type: 'object', + description: 'update information', + properties: { + count: { + type: 'number', + description: 'number of records updated' + } + } + }, + }, + }, + }, +} +``` +Update Controller method: +```javascript +async updateAccount(where, data) { + return await this.repository.update(JSON.parse(where), data); +} +``` + +Please See [Testing Your Application](Testing-Your-Application.md) section in order to set up and write unit, acceptance, and integration tests for your application. + +## Persisting Data without Juggler [Using MySQL database] +LoopBack 4 gives you the flexibility to create your own custom Datasources which utilize your own custom connector for your favourite back end database. You can then fine tune your CRUD methods to your liking. + +### Example Application +You can look at [the account-without-juggler application as an example.](https://github.com/strongloop/loopback-next-example/tree/master/services/account-without-juggler) + +### Steps to create your own concrete DataSource + +1. Implement the `CrudConnector` interface from `@loopback/repository` package. [Here is one way to do it](https://github.com/strongloop/loopback-next-example/blob/master/services/account-without-juggler/repositories/account/datasources/mysqlconn.ts) +2. Implement the `DataSource` interface from `@loopback/repository`. To implement the `DataSource` interface, you must give it a name, supply your custom connector class created in the previous step, and instantiate it: + ```javascript + export class MySQLDs implements DataSource { + name: 'mysqlDs' + connector: MySqlConn + settings: Object + + constructor() { + this.settings = require('./mysql.json'); //connection configuration + this.connector = new MySqlConn(this.settings); + } + } + ``` +3. Extend `CrudRepositoryImpl` class from `@loopback/repository` and supply your custom DataSource and model to it: + + ```javascript + import { CrudRepositoryImpl } from '@loopback/repository'; + import { MySQLDs } from './datasources/mysqlds'; + import { Account } from './models/Account'; + + export class newRepository extends CrudRepositoryImpl { + constructor() { + let ds = new MySQLDs(); + super(ds, Account); + } + } + ``` + +You can override the functions it provides, which ultimately call on your connector's implementation of them, or write new ones. + +### Configure Controller + +The next step is to wire your new DataSource to your controller. +This step is essentially the same as above, but can also be done as follows using DI: + +1. Bind instance of your repository to a certain key in your application class + + ```javascript + class AccountMicroservice extends Application { + private _startTime: Date; + + constructor() { + super(); + const app = this; + app.controller(AccountController); + app.bind('repositories.account').toClass(AccountRepository); + } + ``` + +2. Inject the bound instance into the repository property of your controller. `inject` can be imported from `@loopback/context`. + + ```javascript + export class AccountController { + @inject('repositories.account') private repository: newRepository; + } + ``` + +### Example custom connector CRUD methods + +Here is an example of a `find` function which uses the node-js `mysql` driver to retrieve all the rows that match a particular filter for a model instance. + +```javascript +public find( + modelClass: Class, + filter: Filter, + options: Options +): Promise { + let self = this; + let sqlStmt = "SELECT * FROM " + modelClass.name; + if (filter.where) { + let sql = "?? = ?"; + let formattedSql = ""; + for (var key in filter.where) { + formattedSql = mysql.format(sql, [key, filter.where[key]]); + } + sqlStmt += " WHERE " + formattedSql; + } + debug("Find ", sqlStmt); + return new Promise(function(resolve, reject) { + self.connection.query(sqlStmt, function(err: any, results: Account[]) { + if (err !== null) return reject(err); + resolve(results); + }); + }); +} +``` diff --git a/docs/Reserved-binding-keys.md b/docs/Reserved-binding-keys.md new file mode 100644 index 000000000000..11d003cfb41f --- /dev/null +++ b/docs/Reserved-binding-keys.md @@ -0,0 +1,189 @@ +--- +lang: en +title: 'Reserved binding keys' +keywords: LoopBack 4.0, LoopBack 4 +toc_level: 1 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Reserved-binding-keys.html +summary: +--- +## Overview + +When using [dependency injection](Dependency-injection.md) there are a few things to keep in mind with regards to binding keys. + +Different packages and components for LoopBack 4 may have some bindings already defined. You can change the default behavior by overriding the default binding, but you must ensure the interface of the new binding is the same as the default (but behavior can be different). + +Following is a list that documents the binding keys in use by various `@loopback` packages and their `Type` so you can easily look at their interface in the [API Docs](http://apidocs.loopback.io). + +It is recommended to use the CONSTANT defined for each binding key in it's respective namespace. You can import a namespace and access the binding key in your application as follows: + +```js +import { BindingKeyNameSpace } from 'package-name'; +app.bind(BindKeyNameSpace.KeyName).to('value'); +``` + +{% include note.html title="Declaring new binding keys" content="For component developers creating a new Binding, to avoid conflict with other packages, it is recommended that the binding key start with the package name as the prefix. Example: `@loopback/authentication` component uses the prefix `authentication` for its binding keys. +" %} + +## Package: authentication + +**Reserved prefixes:** + +``` +authentication.* +``` + +### CONSTANT Namespace + +```js +import { AuthenticationBindings } from '@loopback/authentication' +``` + +### Binding keys + +**Sequence Actions binding keys** + +|Name|CONSTANT|`Type `|Description| +|---|---|---|---| +|`authentication.actions.authenticate`|`AUTH_ACTION`|`AuthenticateFn`|Provides the authenticate function to be called in Sequence action.| + +**Other binding keys** + +|Name|CONSTANT|Type|Description| +|---|---|---|---| +|`authentication.currentUser`|`CURRENT_USER`|`UserProfile`|Authenticated user profile for the current request| +|`authentication.operationMetadata`|`METADATA`|`AuthenticationMetadata`|Authentication Metadata| +|`authentication.strategy`|`STRATEGY`|`Strategy`|Provider for a [passport](http://passportjs.org/) strategy| + +## Package: context + +**Reserved prefixes:** + +``` +context.* +``` + +### Binding keys + +_None_ + +## Package: core + +**Reserved prefixes:** + +``` +core.* +``` + +``` +controllers.* +``` + +### CONSTANT Namespace + +```js +import { CoreBindings } from '@loopback/authentication' +``` + +### Binding keys + +|Name|CONSTANT|Type|Description| +|---|---|---|---| +|`application.apiSpec`|`API_SPEC`|`OpenApiSpec`|OpenAPI Specification describing your application's routes| +|`bindElement`|`BIND_ELEMENT`|`BindElement`|Convenience provider function to bind value to `Context`| +|`components.${component.name}`||`Component`|Components used by your application| +|`controllers.${controller.name}`||`ControllerClass`|The controller's bound to the application| +|`controller.current.ctor`|`CONTROLLER_CLASS`|`ControllerClass`|The controller for the current request| +|`controller.current.operation`|`CONTROLLER_METHOD_NAME`|`string`|Name of the operation for the current request| +|`controller.method.meta`|`CONTROLLER_METHOD_META`|`ControllerMetaData`|Metadata for a given controller| +|`getFromContext`|`GET_FROM_CONTEXT`|`GetFromContext`|Convenience provider function to return the `BoundValue` from the `Context`| + +## Package: rest + +|`rest.handler`|`HANDLER`|`HttpHandler`|The HTTP Request Handler| +|`rest.port`|`PORT`|`number`|HTTP Port the application will run on| +|`rest.http.request`|`Http.REQUEST`|`ServerRequest`|The raw `http` request object| +|`rest.http.request.context`|`Http.CONTEXT`|`Context`|Request level context| +|`rest.http.response`|`Http.RESPONSE`|`ServerResponse`|The raw `http` response object| +|`routes.${route.verb}.${route.path}`||`RouteEntry`|Route entry specified in api-spec| +|`rest.sequence`|`SEQUENCE`|`SequenceHandler`|Class that implements the sequence for your application| + +**Rest Sequence Action Binding Keys** + +To use the Rest Sequence Actions CONSTANTs, bind/inject to `RestBindings.SequenceActions.CONSTANT` *OR* + +```js +const SequenceActions = RestBindings.SequenceActions; +SequenceActions.CONSTANT // CONSTANT to bind/inject +``` + +|Name|CONSTANT|Type|Description| +|---|---|---|---| +|`sequence.actions.findRoute`|`FIND_ROUTE`|`FindRoute`|Sequence action to find the route for a given request| +|`sequence.actions.invokeMethod`|`INVOKE_METHOD`|`InvokeMethod`|Sequence action to invoke the operation method defined for the requested route| +|`sequence.actions.logError`|`LOG_ERROR`|`LogError`|Sequence action to log information about a failed request| +|`sequence.actions.parseParams`|`PARSE_PARAMS`|`ParseParams`|Sequence action to parse a request for arguments to be passed to the controller| +|`sequence.actions.reject`|`REJECT`|`Reject`|Sequence action to reject the request with an error| +|`sequence.actions.send`|`SEND`|`Send`|Sequence action to send the response back to client| + +## Package: openapi-spec + +**Reserved prefixes:** + +``` +api-spec.* +``` + +### Binding keys +_None_ + +## Package: openapi-spec-builder + +**Reserved prefixes:** + +``` +spec-builder.* +``` + +### Binding keys +_None_ + +## Package: repository + +**Reserved prefixes:** + +``` +repository.* +``` + +``` +repositories.*` +``` + +``` +datasources.* +``` + +``` +models.* +``` + +### Binding keys + +|Name|CONSTANT|Type|Description| +|---|---|---|---| +|`datasources.${dataSourceName}`||`DataSource`|Instance of a given datasource| +|`models.${modelName}`||`Model`|Instance of a given model| +|`repositories.${repositoryName}`||`Repository`|Instance of a given repository| + +## Package: testlab + +**Reserved prefixes:** + +``` +testlab.* +``` + +### Binding keys +_None_ diff --git a/docs/Roadmap.md b/docs/Roadmap.md new file mode 100644 index 000000000000..9df771e555a4 --- /dev/null +++ b/docs/Roadmap.md @@ -0,0 +1,11 @@ +--- +lang: en +title: Roadmap +keywords: LoopBack 4.0 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Roadmap.html +summary: LoopBack 4.0 is the upcoming release. +--- + +TBD. diff --git a/docs/Routes.md b/docs/Routes.md new file mode 100644 index 000000000000..9d6e2db4bfb2 --- /dev/null +++ b/docs/Routes.md @@ -0,0 +1,167 @@ +--- +lang: en +title: 'Routes' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Routes.html +summary: +--- + +## Overview + +A `Route` is the mapping between your API specification and an Operation (JavaScript implementation). It tells LoopBack which function to `invoke()` given an HTTP request. + +The `Route` object and its associated types are provided as a part of the +[(`@loopback/rest`)](https://github.com/strongloop/loopback-next/blob/master/packages/rest) package. + +## Operations + +Operations are JavaScript functions that accept Parameters. They can be implemented as plain JavaScript functions or as methods in [Controllers](Controllers.md). + +```js +// greet is a basic operation +function greet(name) { + return `hello ${name}`; +} +``` + +## Parameters + +In the example above, `name` is a Parameter. Parameters are values, usually parsed from a `Request` by a `Sequence`, passed as arguments to an Operation. Parameters are defined as part of a `Route` using the OpenAPI specification. They can be parsed from the following parts of the `Request`: + + - `body` + - `query` string + - `header` + - `path` (url) + +## Creating REST Routes + +There are three distinct approaches for defining your REST Routes: +- With an OpenAPI specification object +- Using partial OpenAPI spec fragments with the `Route` constructor +- Using route decorators on controller methods + +### Declaring REST Routes with API specifications + +Below is an example [Open API Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object) that defines the same operation as the example above. This a declarative approach to defining operations. The `x-operation` field in the example below references the handler JavaScript function for the API operation, and should not be confused with `x-operation-name`, which is a string for the Controller method name. + +```js + +const server = await app.getServer(RestServer); +const spec = { + basePath: '/', + paths: { + '/': { + get: { + 'x-operation': greet, + parameters: [{name: 'name', in: 'query', type: 'string'}], + responses: { + '200': { + description: 'greeting text', + schema: {type: 'string'}, + } + } + } + } + } +}; + +server.api(spec); +``` + +### Using partial OpenAPI spec fragments + +The example below defines a `Route` that will be matched for `GET /`. When the `Route` is matched, the `greet` Operation (above) will be called. It accepts an OpenAPI [OperationObject](https://github.com/OAI/OpenAPI-Specification/blob/0e51e2a1b2d668f434e44e5818a0cdad1be090b4/versions/2.0.md#operationObject) which is defined using `spec`. +The route is then attached to a valid server context running underneath the +application. +```ts +import {RestApplication, RestServer, Route} from '@loopback/rest'; +import {OperationObject} from '@loopback/openapi-spec'; + +const app = new RestApplication(); +const spec: OperationObject = { + parameters: [{name: 'name', in: 'query', type: 'string'}], + responses: { + '200': { + description: 'greeting text', + schema: {type: 'string'} + } + } +}; + +// greet is a basic operation +function greet(name: string) { + return `hello ${name}`; +} + +(async function start() { + const server = await app.getServer(RestServer); + const route = new Route('get', '/', spec, greet); + server.route(route); + await app.start(); +})(); +``` + +### Using Route decorators with controller methods + +You can decorate your controller functions using the verb decorator functions +within `@loopback/rest` to determine which routes they will handle. + +{% include code-caption.html content="/src/controllers/greet.controller.ts" %} +```ts +import { get, param } from '@loopback/rest'; + +export class GreetController { + // Note that we can still use OperationObject fragments with the + // route decorators for fine-tuning their definitions and behaviours. + // This could simply be `@get('/')`, if desired. + @get('/', { + responses: { + '200': { + description: 'greeting text', + schema: { type: 'string' } + } + } + }) + @param.query.string('name') + greet(name: string) { + return `hello ${name}`; + } +} +``` + +{% include code-caption.html content="index.ts" %} +```ts +import { RestApplication } from '@loopback/rest'; +import { GreetController } from './src/controllers/greet.controller'; + +const app = new RestApplication(); + +app.controller(GreetController); + +(async function start() { + await app.start(); +})(); +``` + +## Invoking operations using Routes + +This example breaks down how `Sequences` determine and call the matching operation for any given request. + +```js +class MySequence extends DefaultSequence { + async handle(request, response) { + // find the route that matches this request + const route = this.findRoute(request); + + // params is created by parsing the request using the route + const params = this.parseParams(request, route); + + // invoke() uses both route and params to execute the operation specified by the route + const result = await this.invoke(route, params); + + await this.send(response, result); + } +} +``` diff --git a/docs/Sequence.md b/docs/Sequence.md new file mode 100644 index 000000000000..92347320bfe6 --- /dev/null +++ b/docs/Sequence.md @@ -0,0 +1,280 @@ +--- +lang: en +title: 'Sequence' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Sequence.html +summary: +--- + +## What is a Sequence? + +A `Sequence` is a stateless grouping of [Actions](#actions) that control how a +`Server` responds to requests. + +The contract of a `Sequence` is simple: it must produce a response to a request. +Creating your own `Sequence` gives you full control over how your `Server` +instances handle requests and responses. The `DefaultSequence` looks like this: + + +```js +class DefaultSequence { + async handle(request: ParsedRequest, response: ServerResponse) { + try { + const route = this.findRoute(request); + const params = await this.parseParams(request, route); + const result = await this.invoke(route, params); + await this.send(response, result); + } catch(err) { + await this.reject(response, err); + } + } +} +``` + +## Elements + +In the example above, `route`, `params`, and `result` are all Elements. When building sequences, you use LoopBack Elements to respond to a request: + +- [`Route`](http://apidocs.loopback.io/@loopback%2frest/#Route) +- [`Request`](http://apidocs.strongloop.com/loopback-next/) - (TBD) missing API docs link +- [`Response`](http://apidocs.strongloop.com/loopback-next/) - (TBD) missing API docs link +- [`OperationRetVal`](http://apidocs.loopback.io/@loopback%2frest/#OperationRetval) +- [`Params`](http://apidocs.strongloop.com/loopback-next/) - (TBD) missing API docs link +- [`OpenAPISpec`](http://apidocs.loopback.io/@loopback%2fopenapi-spec/) +- [`OperationError`](http://apidocs.strongloop.com/loopback-next/OperationError) - (TBD) missing API docs link +- [`OperationMeta`](http://apidocs.strongloop.com/loopback-next/OperationMeta) - (TBD) missing API docs link +- [`OperationRetMeta`](http://apidocs.strongloop.com/loopback-next/OperationRetMeta) - (TBD) missing API docs link + +## Actions + +Actions are JavaScript functions that only accept or return `Elements`. Since the input of one action (an Element) is the output of another action (Element) you can easily compose them. Below is an example that uses several built-in Actions: + +```js +class MySequence extends DefaultSequence { + async handle(request: ParsedRequest, response: ServerResponse) { + // findRoute() produces an element + const route = this.findRoute(request); + // parseParams() uses the route element and produces the params element + const params = await this.parseParams(request, route); + // invoke() uses both the route and params elements to produce the result (OperationRetVal) element + const result = await this.invoke(route, params); + // send() uses the result element + await this.send(response, result); + } +} +``` + +## Custom Sequences + +Most use cases can be accomplished with `DefaultSequence` or by slightly customizing it: + +```js +class MySequence extends DefaultSequence { + log(msg) { + console.log(msg); + } + async handle(request, response) { + this.log('before request'); + await super.handle(request, response); + this.log('after request'); + } +} +``` + +In order for LoopBack to use your custom sequence, you must register it on any +applicable `Server` instances before starting your `Application`: + +```js +import {RestApplication, RestServer} from '@loopback/rest'; + +const app = new RestApplication(); + +// or +(async function start() { + const server = await app.getServer(RestServer); + server.sequence(MySequence); + await app.start(); +})(); +``` + +## Advanced topics + +### Custom routing + +A custom `Sequence` enables you to control exactly how requests are routed to endpoints such as `Controller` methods, plain JavaScript functions, Express applications, and so on. + +This example demonstrates determining which endpoint (controller method) to invoke based on an API specification. + +```ts +import {findRoute} from '@loopback/rest' + +const API_SPEC = { + basePath: '/', + paths: { + '/greet': { + get: { + 'x-operation-name': "greet", + responses: { + 200: { + description: "greeting text", + schema: { type: "string" } + } + } + } + } + } +}; + +class MySequence extends DefaultSequence { + async run(request) { + const {methodName} = this.findRoute(request, API_SPEC); + await this.sendResponse(methodName); // => greet + } +} +``` + +### Customizing Sequence Actions + +There might be scenarios where the default sequence _ordering_ is not something +you want to change, but rather the individual actions that the sequence will +execute. + +To do this, you'll need to override one or more of the sequence action bindings +used by the `RestServer`, under the `RestBindings.SequenceActions` constants. + +As an example, we'll implement a custom sequence action to replace the default +"send" action. This action is responsible for returning the response from a +controller to the client making the request. + +To do this, we'll register a custom send action by binding a +[Provider](http://apidocs.strongloop.com/@loopback%2fcontext/#Provider) to the +`RestBindings.SequenceActions.SEND` key. + +First, let's create our `CustomSendProvider` class, which will provide the +send function upon injection. + +{% include code-caption.html content="/src/providers/custom-send-provider.ts" %} +**custom-send-provider.ts** +```ts +import {Send, ServerResponse} from "@loopback/rest"; +import {Provider, BoundValue, inject} from "@loopback/context"; +import {writeResultToResponse, RestBindings} from "@loopback/rest"; + +// Note: This is an example class; we do not provide this for you. +import {Formatter} from "../utils"; + +export class CustomSendProvider implements Provider { + // In this example, the injection key for formatter is simple + constructor( + @inject('utils.formatter') public formatter: Formatter, + @inject(RestBindings.Http.REQUEST) public request: Request, + ) {} + + value(): Send | Promise { + // Use the lambda syntax to preserve the "this" scope for future calls! + return (response, result) => { + this.action(response, result); + }; + } + /** + * Use the mimeType given in the request's Accept header to convert + * the response object! + * @param ServerResponse response The response object used to reply to the + * client. + * @param OperationRetVal result The result of the operation carried out by + * the controller's handling function. + */ + action(response: ServerResponse, result: OperationRetVal) { + if (result) { + // Currently, the headers interface doesn't allow arbitrary string keys! + const headers = this.request.headers as any || {}; + const header = headers.accept || 'application/json'; + const formattedResult = + this.formatter.convertToMimeType(result, header); + response.setHeader('Content-Type', header); + response.end(formattedResult); + } else { + response.end(); + } + } +} +``` + +Our custom provider will automatically read the `Accept` header from the request +context, and then transform the result object so that it matches the specified +MIME type. + +Next, in our application class, we'll inject this provider on the +`RestBindings.SequenceActions.SEND` key. + +{% include code-caption.html content="/src/application.ts" %} +```ts +import {Application} from '@loopback/core'; +import {RestApplication, RestBindings} from '@loopback/rest'; +import {RepositoryMixin} from '@loopback/repository'; +import {CustomSendProvider} from './providers/custom-send-provider'; +import {Formatter} from './utils'; +import {BindingScope} from '@loopback/context'; + +export class YourApp extends RepositoryMixin(RestApplication) { + constructor() { + super(); + // Assume your controller setup and other items are in here as well. + this.bind('utils.formatter').toClass(Formatter) + .inScope(BindingScope.SINGLETON); + this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider); + } +``` + +As a result, whenever the send action of the +[`DefaultSequence`](http://apidocs.strongloop.com/@loopback%2frest/#DefaultSequence) +is called, it will make use of your function instead! You can use this approach +to override any of the actions listed under the `RestBindings.SequenceActions` +namespace. + +### Query string parameters + +{% include content/tbd.html %} + +How to get query string param values. + +### Parsing Requests + +{% include content/tbd.html %} + +Parsing and validating arguments from the request url, headers, and body. + +### Invoking controller methods + +{% include content/tbd.html %} + + - How to use `invoke()` in simple and advanced use cases. + - Explain what happens when you call `invoke()` + - Mention caching use case + - Can I call invoke multiple times? + +### Writing the response + +{% include content/tbd.html %} + + - Must call `sendResponse()` exactly once + - Streams? + +### Sending errors + +{% include content/tbd.html %} + + - try/catch details + +### Keeping your Sequences + +{% include content/tbd.html %} + + - Try and use existing actions + - Implement your own version of built in actions + - Publish reusable actions to npm diff --git a/docs/Server.md b/docs/Server.md new file mode 100644 index 000000000000..50a978f6af0f --- /dev/null +++ b/docs/Server.md @@ -0,0 +1,70 @@ +--- +lang: en +title: 'Server' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Server.html +summary: +--- + +## Overview + +The [Server](https://apidocs.strongloop.com/@loopback%2fcore/#Server) interface defines the minimal required functions (start and stop) to implement for a LoopBack application. Servers in LoopBack 4 are used to represent implementations for inbound transports and/or protocols such as REST over http, gRPC over http2, graphQL over https, etc. They typically listen for requests on a specific port, handle them, and return appropriate responses. A single application can have multiple server instances listening on different ports and working with different protocols. + + +## Usage + +LoopBack 4 currently offers the [`@loopback/rest`](https://github.com/strongloop/loopback-next/tree/master/packages/rest) package out of the box which provides an HTTP based server implementation handling requests over REST called `RestServer`. In order to use it in your application, all you need to do is have your application class extend `RestApplication`, and it will provide you with an instance of RestServer listening on port 3000. The following shows how to make use of it: + +```ts +import {RestApplication, RestServer} from '@loopback/rest'; + +export class HelloWorldApp extends RestApplication { + constructor() { + super(); + } + + async start() { + // get a singleton HTTP server instance + const rest = await this.getServer(RestServer); + // give our RestServer instance a sequence handler function which + // returns the Hello World string for all requests + rest.handler((sequence, request, response) => { + sequence.send(response, 'Hello World!'); + }); + // call start on application class, which in turn starts all registered + // servers + await super.start(); + console.log(`REST server running on port: ${await rest.get('rest.port')}`); + } +} +``` + +## Configuration + +### Add servers to application instance + +You can add server instances to your application via the `app.server()` method individually or as an array using `app.servers()` method. Using `app.server()` allows you to uniquely name your binding key for your specific server instance. The following example demonstrates how to use these functions: + +```ts +import {RestApplication, RestServer} from '@loopback/rest'; + +export class HelloWorldApp extends RestApplication { + constructor() { + super(); + // This server instance will be bound under "servers.fooServer". + this.server(RestServer, 'fooServer'); + // Creates a binding for "servers.MQTTServer" and a binding for + // "servers.SOAPServer"; + this.servers([MQTTServer, SOAPServer]); + } +} +``` + +You can also add multiple servers in the constructor of your application class as shown [here](Application.md#servers). + +## Next Steps + +- Learn about [Server-level Context](Context.md#server-level-context) +- Learn more about [creating your own servers!](Creating-components.md#creating-your-own-servers) diff --git a/docs/Team.md b/docs/Team.md new file mode 100644 index 000000000000..e2f404415e4b --- /dev/null +++ b/docs/Team.md @@ -0,0 +1,20 @@ +--- +lang: en +title: Team +keywords: LoopBack 4.0 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Team.html +summary: The people working on LoopBack 4 +--- + +# IBM + +- Core members/leads +- All internal members (including docs, etc) + +# Community contributors + +- loopback-maintaineres +- loopback-swagger-maintainers +- etc diff --git a/docs/Testing-Your-Extensions.md b/docs/Testing-Your-Extensions.md new file mode 100644 index 000000000000..a80439e96c05 --- /dev/null +++ b/docs/Testing-Your-Extensions.md @@ -0,0 +1,280 @@ +--- +lang: en +title: 'Testing your extension' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Testing-your-extension.html +summary: +--- + +## Overview + +LoopBack 4 extensions are often used by other teams. A thorough test suite for your extension brings powerful benefits to all your users, including: + +* Validating the behavior of the extension +* Preventing unwanted changes to the API or functionality of the extension +* Providing working samples and code snippets that serve as functional documentation for your users + +## Project Setup + +We recommend that you use `@loopback/cli` to create the extension, as it installs several tools you can use for testing, such as `mocha`, assertion libraries, and a swagger validator. + +The `@loopback/cli` includes the `mocha` automated test runner and a +`test` folder containing recommended folders for various types of tests. +`Mocha` is enabled by default if `@loopback/cli` is used to +create the extension project. The `@loopback/cli` installs and configures `mocha`, creates the `test` folder, and also enters a `test` command in your `package.json`. + +Assertion libraries such as [ShouldJS](http://shouldjs.github.io/) (as `expect`), +[SinonJS](http://sinonjs.org/), and a swagger validator are made available +through the convenient `@loopback/testlab` package. The `testlab` is also installed by `@loopback/cli`. + +### Manual Setup - Using Mocha + +* Install `mocha` by running `npm i --save-dev mocha`. This will save the `mocha` package in +`package.json` as well. +* Under `scripts` in `package.json` add the following: +`test: npm run build && mocha --recursive ./dist/test` + +## Types of tests + +A comprehensive test suite tests many aspects of your code. We recommend that you write unit, integration, and acceptance tests to test your application from a variety of perspectives. Comprehensive testing +ensures correctness, integration, and future compatibility. + +You may use any development methodology you want to write your extension; the +important thing is to test it with an automated test suite. In Traditional development methodology, you write the code first and then write the tests. In Test-driven development methodology, you write the tests first, see them fail, then write the code to pass the tests. + +### Unit Tests + +A unit test tests the smallest unit of code possible, which in this case is a function. +Unit tests ensure variable and state changes by outside actors don't affect the +results. [Test doubles](https://en.wikipedia.org/wiki/Test_double) should be +used to substitute function dependencies. You can learn more about test doubles +and Unit testing here: [Testing your Application: Unit testing](Testing-your-application.md#unit-testing). + +#### Controllers + +At its core, a controller is a simple class that is responsible for related +actions on an object. Performing unit tests on a controller in an extension is the same as +performing unit tests on a controller in an application. + +To test a controller, you instantiate a new instance of your controller +class and test a function, providing a test double for constructor arguments as +needed. Following are examples that illustrate how to perform a unit test on a controller class: + +**`src/controllers/ping.controller.ts`** +```ts +export class PingController { + @get('/ping') + ping(msg?: string) { + return `You pinged with ${msg}`; + } +} +``` + +**`test/unit/controllers/ping.controller.ts`** +```ts +import {PingController} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('PingController() unit', () => { + it('pings with no input', () => { + const controller = new PingController(); + const result = controller.ping(); + expect(result).to.equal('You pinged with undefined'); + }); + + it('pings with msg \'hello\'', () => { + const controller = new PingController(); + const result = controller.ping('hello'); + expect(result).to.equal('You pinged with hello'); + }); +}); +``` + +You can find an advanced example on testing controllers in [Unit test your Controllers](Testing-your-application.md#unit-test-your-controllers). + +#### Decorators + +The recommended usage of a decorator is to store metadata about a class or a +class method. The decorator implementation usually provides a function +to retrieve the related metadata based on the class name and method name. +For a unit test for a decorator, it is important to test that that it stores and retrieves the correct metadata. *The retrieval gets tested as a +result of validating whether the metadata was stored or not.* + +Following is an example for testing a decorator: + +**`src/decorators/test.decorator.ts`** +```ts +export function test(file: string) { + return function(target: Object, methodName: string): void { + Reflector.defineMetadata( + 'example.msg.decorator.metadata.key', + {file}, + target, + methodName, + ); + }; +} + +export function getTestMetadata( + controllerClass: Constructor<{}>, + methodName: string, +): {file: string} { + return Reflector.getMetadata( + 'example.msg.decorator.metadata.key', + controllerClass.prototype, + methodName, + ); +} +``` + +**`test/unit/decorators/test.decorator.ts`** +```ts +import {test, getTestMetadata} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('test.decorator (unit)', () => { + it('can store test name via a decorator', () => { + class TestClass { + @test('me.test.ts') + me() {} + } + + const metadata = getTestMetadata(TestClass, 'me'); + expect(metadata).to.be.a.Object(); + expect(metadata.file).to.be.eql('me.test.ts'); + }); +}); +``` + +#### Mixins + +A Mixin is a TypeScript function that extends the `Application` Class, adding +new constructor properties, methods, etc. It is difficult to write a unit test +for a Mixin without the `Application` Class dependency. The recommended practice +is to write an integration test is described in [Mixin Integration Tests](#mixin-integration-tests). + +#### Providers + +A Provider is a Class that implements the `Provider` interface. This interface +requires the Class to have a `value()` function. A unit test for a provider +should test the `value()` function by instantiating a new `Provider` class, using +a test double for any constructor arguments. + +**`src/providers/random-number.provider.ts`** +```ts +import {Provider} from '@loopback/context'; + +export class RandomNumberProvider implements Provider { + value(): number { + return (max: number): number => { + return Math.floor(Math.random() * max) + 1; + }; + } +} +``` + +**`test/unit/providers/random-number.unit.test.ts`** +```ts +import {RandomNumberProvider} from '../../..'; +import {expect} from '@loopback/testlab'; + +describe('RandomNumberProvider (unit)', () => { + it('generates a random number within range', () => { + const provider = new RandomNumberProvider().value(); + const random: number = provider(3); + + expect(random).to.be.a.Number(); + expect(random).to.equalOneOf([1, 2, 3]); + }) +}) +``` + +#### Repositories + +*This section will be provided in a future version.* + +### Integration Tests + +An integration test plays an important part in your test suite by ensuring your +extension artifacts work together as well as `@loopback`. It is +recommended to test two items together and substitute other integrations as test doubles so it becomes apparent where the integration errors may occur. + +#### Mixin Integration Tests + +A Mixin extends a base Class by returning an anonymous class. Thus, a Mixin is tested by actually +using the Mixin with its base Class. Since this requires two Classes to work +together, an integration test is needed. A Mixin test checks that +new or overridden methods exist and work as expected in the new Mixed class. Following is an example for an +integration test for a Mixin: + +**`src/mixins/time.mixin.ts`** +```ts +import {Constructor} from '@loopback/context'; +export function TimeMixin>(superClass: T) { + return class extends superClass { + constructor(...args: any[]) { + super(...args); + if (!this.options) this.options = {}; + + if (typeof this.options.timeAsString !== 'boolean') { + this.options.timeAsString = false; + } + } + + time() { + if (this.options.timeAsString) { + return new Date().toString(); + } + return new Date(); + } + }; +} +``` + +**`test/integration/mixins/time.intg.test.ts`** +```ts +import {expect} from '@loopback/testlab'; +import {Application} from '@loopback/core'; +import {TimeMixin} from '../../..'; + +describe('TimeMixin (integration)', () => { + it('mixed class has .time()', () => { + const myApp = new AppWithTime(); + expect(typeof myApp.time).to.be.eql('function'); + }); + + it('returns time as string', () => { + const myApp = new AppWithLogLevel({ + timeAsString: true, + }); + + const time = myApp.time(); + expect(time).to.be.a.String(); + }); + + it('returns time as Date', () => { + const myApp = new AppWithLogLevel(); + + const time = myApp.time(); + expect(time).to.be.a.Date(); + }); + + class AppWithTime extends TimeMixin(Application) {} +}); +``` + +### Acceptance Test + +An Acceptance test for an extension is a comprehensive test written +end-to-end. Acceptance tests cover the user scenarios. An acceptance +test uses all of the extension artifacts such as decorators, mixins, +providers, repositories, etc. No test doubles are needed for an +Acceptance test. This is a black box test where you don't know or care about the +internals of the extensions. You will be using the extension as if you were the consumer. + +Due to the complexity of an Acceptance test, there is no example given here. +Have a look at [loopback4-example-log-extension](https://github.com/strongloop/loopback4-example-log-extension) +to understand the extension artifacts and their usage. An Acceptance test can +be seen here: [test/acceptance/log.extension.acceptance.ts](https://github.com/strongloop/loopback4-example-log-extension/blob/master/test/acceptance/log.extension.acceptance.ts). diff --git a/docs/Testing-the-API.md b/docs/Testing-the-API.md new file mode 100644 index 000000000000..e270bd1b0cce --- /dev/null +++ b/docs/Testing-the-API.md @@ -0,0 +1,200 @@ +--- +lang: en +title: 'Testing the API' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Testing-the-API.html +summary: +--- + +{% include previous.md content=" +This article continues off from [Defining and validating the API](./Defining-and-validating-the-API.md). +" %} + +{% include important.html content="The top-down approach for building LoopBack +applications is not yet fully supported. Therefore, the steps outlined in this +page are outdated and may not work out of the box. They will be revisited after +our MVP release. +"%} + +## Smoke test API input/output + +Once you confirm that the API specification is valid, it's time to verify that the application implements the API as you have specified it. The input/output testing described below uses [Dredd](https://www.npmjs.com/package/dredd), specifically `hello-world` in this section. Concrete sample code of `hello-world` can be found in the [hello-world tutorial](https://github.com/strongloop/loopback-next-hello-world) repository. Although the sample code includes a validated API spec and fully functional `hello-world` controller, let's pretend the controller is completely empty. Try it yourself by cloning the repository from GitHub. + +For input/output testing, you are going to create three parts: +1. Input data definition. +2. Expected output response definition. +3. Test code. + +Parts one and two are included in the API specification. The input data is given as `x-example` as follows: + +```js +"x-example": "Ted" +``` + +The expected output as `examples`: + +```js +"examples": { + "text/plain": "Hello world Ted." +} +``` + +The `Dredd` module reserves `x-example` to set the input parameter. the OpenAPI standard defines the [`examples` object](https://swagger.io/specification/#examples-object-92) as a map from `MIME type` to the content value. Here, it's `text/plain` MIME type. As you see, they are a pair: When you change the input value `x-example`, you must change `examples` value as well. + +The complete `hello-world` API specification is the following: + +```js +export const controllerSpec = +{ + swagger: '2.0', + basePath: '/', + info: { + title: 'LoopBack Application', + version: '1.0.0', + }, + "paths": { + "/helloworld": { + "get": { + "x-operation-name": "helloWorld", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Your name.", + "required": false, + "type": "string", + "x-example": "Ted" + } + ], + "responses": { + "200": { + "description": "Returns a hello world with your (optional) name.", + "examples": { + "text/plain": "Hello world Ted." + } + } + } + } + } + } +} +``` + +The third piece is the test code. To initialize the test environment, you need to create a `Dredd` instance specifying the configuration. There are two required fields in the configuration object: `server` and `options.path`. + `localhostAndPort + \'/swagger.json\'` is the predefined end point LoopBack 4 uses for the client to access the API specification of the service API. + +```js + async function initEnvironment() { + // By default, the port is set to 3000. + const app: Application = new HelloWorldApp(); + const server = app.getServer(RestServer); + // For testing, you'll let the OS pick an available port by setting + // RestBindings.PORT to 0. + server.bind(RestBindings.PORT).to(0); + // app.start() starts up the HTTP server and binds the acquired port + // number to RestBindings.PORT. + await app.start(); + // Get the real port number. + const port: number = await server.get(RestBindings.PORT); + const localhostAndPort: string = 'http://localhost:' + port; + const config: object = { + server: localhostAndPort, // base path to the end points + options: { + level: 'fail', // report 'fail' case only + silent: false, // false for helpful debugging info + path: [localhostAndPort + '/swagger.json'], // to download apiSpec from the service + } + }; + dredd = new Dredd(config); + } +``` + +Since the specification above includes definition of input data and the expected output, you have all the pieces to write the test code: + +```js +describe('Api Spec End Points', () => { + let dredd: any; + before(initEnvironment); + + describe('input/output test', () => { + + it('passes match', done => { + dredd.run((err: Error, stats: object) => { + if (err) return done(err); + expect(stats).to.containDeep({ + failures: 0, + errors: 0, + skipped: 0, + }); + done(); + }); + }); + }); + + async function initEnvironment() { + // + // see initEnvironment defined above. + // + }); +}) +``` + +Try running the first test: + +```shell +$ npm test + + Api Spec End Points + input/output test +fail: GET /helloworld?name=Ted duration: 26ms +fail: body: Real and expected data does not match. + +request: +method: GET +uri: /helloworld?name=Ted +headers: + User-Agent: Dredd/4.3.0 (Darwin 16.7.0; x64) + Content-Length: 0 +body: + +expected: +headers: + Content-Type: text/plain +body: +Hello world Ted +statusCode: 200 + +actual: +statusCode: 500 +headers: + date: Wed, 23 Aug 2017 00:17:48 GMT + connection: close + content-length: 0 +body: + +complete: 0 passing, 1 failing, 0 errors, 0 skipped, 1 total +complete: Tests took 27ms +``` + +The test report correctly shows that the input is `name=Ted` and the expected result is `Hello world Ted`, but the actual result was `statusCode: 500` which does not match the expectation. When the `hello-world` API is implemented, the result would be something like the following: + +```shell +$ npm test + + Api Spec End Points + input/output test +complete: 1 passing, 0 failing, 0 errors, 0 skipped, 1 total +complete: Tests took 21ms +``` + +It's a powerful proposition to use the API specification not only for API declaration but for test case declaration. The discussion so far paves the road to "automated controller wireframe-code generation and test-driven development" based on the OpenAPI standard. + +At this point, you are ready to make these tests pass by coding up your business logic. + +Please refer to [Perform an auto-generated smoke test of your REST API](Testing-your-application.md#perform-an-auto-generated-smoke-test-of-your-rest-api) from [Testing your application](Testing-your-application.md) for more details. + +{% include next.html content= " +[Defining your testing strategy](./Defining-your-testing-strategy.md) +" %} diff --git a/docs/Testing-your-application.md b/docs/Testing-your-application.md new file mode 100644 index 000000000000..1bb198d1bf4d --- /dev/null +++ b/docs/Testing-your-application.md @@ -0,0 +1,575 @@ +--- +lang: en +title: 'Testing your application' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Testing-your-application.html +summary: +--- + +## Overview + +A thorough automated test suite is important because it: +- Ensures your application works as expected. +- Prevents regressions when new features are added and bugs are fixed. +- Helps new and existing developers understand different parts of the codebase (knowledge sharing). +- Speeds up development over the long run (the code writes itself!) + +### Types of tests + +We encourage writing tests from a few perspectives, mainly [black-box testing](https://en.wikipedia.org/wiki/Black-box_testing) (acceptance) and [white-box testing](https://en.wikipedia.org/wiki/White-box_testing) (integration and unit). Tests are usually written using typical patterns such as [`arrange/act/assert`](https://msdn.microsoft.com/en-us/library/hh694602.aspx#Anchor_3) or [`given/when/then`](https://martinfowler.com/bliki/GivenWhenThen.html). While both styles work well, just pick one that you're comfortable with and start writing tests! + +For an introduction to automated testing, see [Define your testing strategy](Thinking-in-LoopBack.md#define-your-testing-strategy); for a step-by-step tutorial, see [Incrementally implement features](Thinking-in-LoopBack.md#incrementally-implement-features). + +{% include important.html content="A great test suite requires you to think smaller and favor fast, focused unit tests over slow application-wide end-to-end tests +" %} + +This article is a reference guide for common types of tests and test helpers. + +## Project setup + +An automated test suite requires a test runner to execute all the tests and produce a summary report. We use and recommend [Mocha](https://mochajs.org). + +In addition to a test runner, the test suites generally requires: + +- An assertion library (we recommend [Should.js](https://shouldjs.github.io)). +- A Library for making HTTP calls and verifying their results (we recommend [supertest](https://github.com/visionmedia/supertest)). +- A library for creating test doubles (we recommend [Sinon.JS](http://sinonjs.org/)). + +The [@loopback/testlab](https://www.npmjs.com/package/@loopback/testlab) module +integrates these packages and makes them easy to use together with LoopBack. + +### Set up testing infrastructure with LoopBack CLI + +{% include note.html content="The LoopBack CLI does not yet support LoopBack 4, +so using the CLI is not an option with the beta release. +" %} + + + +### Setup testing infrastructure manually + +If you have an existing application install `mocha` and `@loopback/testlab`: + +``` +npm install --save-dev mocha @loopback/testlab +``` + +Your `package.json` should then look something like this: + +```js +{ + // ... + "devDependencies": { + "@loopback/testlab": "^", + "mocha": "^" + }, + "scripts": { + "test": "mocha" + } + // ... +} +``` + +## Data handling + +Tests accessing a real database often require existing data. For example, a method listing all products needs some products in the database; a method to create a new product instance must determine which properties are required and any restrictions on their values. There are various approaches to address this issue. Many of them unfortunately make the test suite difficult to understand, difficult to maintain, and prone to test failures unrelated to the changes made. + +Based on our experience, we recommend the following approach. + +### Clean the database before each test + +Always start with a clean database before each test. This may seem counter-intuitive: why not reset the database after the test has finished? When a test fails and the database is cleaned after the test has finished, then it's difficult to observe what was stored in the database and why the test failed. When the database is cleaned in the beginning, then any failing test will leave the database in the state that caused the test to fail. + +To clean the database before each test, set up a `beforeEach` hook to call a helper method; for example: + +{% include code-caption.html content="test/helpers/database.helpers.ts" %} +```ts +export async function givenEmptyDatabase() { + await new ProductRepository().deleteAll(); + await new CategoryRepository().deleteAll(); +} + +// in your test file +describe('ProductController (integration)', () => { + before(givenEmptyDatabase); + // etc. +}); +``` + +### Use test data builders + +To avoid duplicating code for creating model data with all required properties filled in, use shared [test data builders](http://www.natpryce.com/articles/000714.html) instead. This enables tests to provide a small subset of properties that are strictly required by the tested scenario, which is important because it makes tests: + +- Easier to understand, since it's immediately clear what model properties are relevant to the test. If the test were setting all required properties, it would be difficult to tell whether some of those properties are actually relevant to the tested scenario. + +- Easier to maintain. As your data model evolves, you eventually need to add more required properties. If the tests were building model instance data manually, you would have to fix all tests to set the new required property. With a shared helper, there is only a single place where to add a value for the new required property. + +See [@loopback/openapi-spec-builder](https://www.npmjs.com/package/@loopback/openapi-spec-builder) for an example of how to apply this design pattern for building OpenAPI Spec documents. + +In practice, a rich method-based API is overkill and a simple function that adds missing required properties is sufficient. + +```ts +export function givenProductData(data: Partial) { + return Object.assign({ + name: 'a-product-name', + slug: 'a-product-slug', + price: 1, + description: 'a-product-description', + available: true, + }, data); +} + +export async function givenProduct(data: Partial) { + return await new ProductRepository().create( + givenProductData(data)); +} +``` + +### Avoid sharing the same data for multiple tests + +It's tempting to define a small set of data that's shared by all tests. For example, in an e-commerce application, you might pre-populate the database with few categories, some products, an admin user and a customer. Such approach has several downsides: + +- When trying to understand any individual test, it's difficult to tell what part of the pre-populated data is essential for the test and what's irrelevant. For example, in a test checking the method counting the number of products in a category using a pre-populated category "Stationery", is it important that "Stationery" contains nested sub-categories or is that fact irrelevant? If it's irrelevant, then what are the other tests that depend on it? + +- As the application grows and new features are added, it's easier to add more properties to existing model instances rather than create new instances using only properties required by the new features. For example, when adding a category image, it's easier to add image to an existing category "Stationery" and perhaps keep another category "Groceries" without any image, rather than create two new categories "CategoryWithAnImage" and "CategoryMissingImage". This further amplifies the previous problem, because it's not clear that "Groceries" is the category that should be used by tests requiring a category with no image - the category name does not provide any hints on that. + +- As the shared dataset grows (together with the application), the time required to bring the database into initial state grows too. Instead of running a few "DELETE ALL" queries before each test (which is relatively fast), you can end up with running tens to hundreds different commands creating different model instances, triggering slow index rebuilds along the way, and considerably slowing the test suite. + +Use the test data builders described in the previous section to populate your database with the data specific to your test only. + +Using the e-commerce example described above, this is how integration tests for the `CategoryRepository` might look: + +```ts +describe('Category (integration)', () => { + beforeEach(givenEmptyDatabase); + + describe('countProducts()', () => { + it('returns correct count for an empty', async () => { + const category = await givenCategory(); + const count = await category.countProducts(); + expect(count).to.equal(0); + }); + + // etc. + + it('includes products in subcategories', async () => { + const category = await givenCategory({ + products: [await givenProduct()], + subcategories: [ + givenCategory({ + products: [await givenProduct()] + }) + ], + }); + + const count = await category.countProducts(); + expect(count).to.equal(2); + }); + }); +}); +``` + +Write higher-level helpers to share the code for re-creating common scenarios. For example, if your application has two kinds of users (admins and customers), then you may write the following helpers to simplify writing acceptance tests checking access control: + +```ts +async function givenAdminAndCustomer() { + return { + admin: await givenUser({role: Roles.ADMIN}), + customer: await givenUser({role: Roles.CUSTOMER}), + }; +} +``` + +## Unit testing + +Unit tests are considered "white-box" tests because they use an "inside-out" approach where the tests know about the internals and controls all the variables of the system being tested. Individual units are tested in isolation, their dependencies are replaced with [Test doubles](https://en.wikipedia.org/wiki/Test_double). + +### Use test doubles + +Test doubles are functions or objects that look and behave like the real variants used in production, but are actually simplified versions giving the test more control of the behavior. For example, reproducing the situation where reading from a file failed because of a hard-drive error is pretty much impossible, unless we are using a test double that's simulating file-system API and giving us control of how what each call returns. + +[Sinon.JS](http://sinonjs.org/) has become the de-facto standard for test doubles in Node.js and JavaScript/TypeScript in general. The `@loopback/testlab` package comes with Sinon preconfigured with TypeScript type definitions and integrated with Should.js assertions. + +There are three kinds of test doubles provided by Sinon.JS: + +- [Test spies](http://sinonjs.org/releases/v4.0.1/spies/) are functions that record arguments, the return value, the value of `this`, and exceptions thrown (if any) for all its calls. There are two types of spies: Some are anonymous functions, while others wrap methods that already exist in the system under test. + +- [Test stubs](http://sinonjs.org/releases/v4.0.1/stubs/) are functions (spies) with pre-programmed behavior. As spies, stubs can be either anonymous, or wrap existing functions. When wrapping an existing function with a stub, the original function is not called. + +- [Test mocks](http://sinonjs.org/releases/v4.0.1/mocks/) (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations. A mock will fail your test if it is not used as expected. + +{% include note.html content="We recommend against using test mocks. With test mocks, the expectations must be defined before the tested scenario is executed, which breaks the recommended test layout 'arrange-act-assert' (or 'given-when-then') and produces code that's difficult to comprehend. +" %} + +#### Create a stub Repository + +When writing an application accessing data in a database, best practice is to use [repositories](Repositories.md) to encapsulate all data-access/persistence-related code and let other parts of the application (typically [controllers](Controllers.md)) to depend on these repositories for data access. To test Repository dependents (for example, Controllers) in isolation, we need to provide a test double, usually as a test stub. + +In traditional object-oriented languages like Java or C#, to enable unit tests to provide a custom implementation of the repository API, the controller needs to depend on an interface describing the API, and the repository implementation needs to implement this interface. The situation is easier in JavaScript and TypeScript. Thanks to the dynamic nature of the language, it’s possible to mock/stub entire classes. + +Creating a test double for a repository class is very easy using the Sinon.JS utility function `createStubInstance`. It's important to create a new stub instance for each unit test in order to prevent unintended re-use of pre-programmed behavior between (unrelated) tests. + +```ts +describe('ProductController', () => { + let repository: ProductRepository; + beforeEach(givenStubbedRepository); + + // your unit tests + + function givenStubbedRepository() { + repository = sinon.createStubInstance(ProductRepository); + } +}); +``` + +In your unit tests, you will usually want to program the behavior of stubbed methods (what should they return) and then verify that the Controller (unit under test) called the right method with the correct arguments. + +Configure stub's behavior at the beginning of your unit test (in the "arrange" or "given" section): + +```ts +// repository.find() will return a promise that +// will be resolved with the provided array +const findStub = repository.find as sinon.SinonStub; +findStub.resolves([{id: 1, name: 'Pen'}]); +``` + +Verify how was the stubbed method executed at the end of your unit test (in the "assert" or "then" section): + +```ts +// expect that repository.find() was called with the first +// argument deeply-equal to the provided object +expect(findStub).to.be.calledWithMatch({where: {id: 1}}); +``` + +See [Unit test your controllers](#unit-test-your-controllers) for a full example. + +#### Create a stub Service + +{% include content/tbd.html %} + +To be done. The initial beta release does not include Services as a first-class feature. + +### Unit test your Controllers + +Unit tests should apply to the smallest piece of code possible to ensure other variables and state changes do not pollute the result. A typical unit test creates a controller instance with dependencies replaced by test doubles and directly calls the tested method. The example below gives the controller a stub implementation of its repository dependency, and then ensure the controller called repository's `find()` method with a correct query and returned back the query results. See [Create a stub repository](#create-a-stub-repository) for a detailed explanation. + +{% include code-caption.html content="test/controllers/product.controller.unit.ts" %} +```ts +import {ProductController, ProductRepository} from '../..'; +import {expect, sinon} from '@loopback/testlab'; + +describe('ProductController (unit)', () => { + let repository: ProductRepository; + beforeEach(givenStubbedRepository); + + describe('getDetails()', () => { + it('retrieves details of a product', async () => { + const controller = new ProductController(repository); + const findStub = repository.find as sinon.SinonStub; + findStub.resolves([{id: 1, name: 'Pen'}]); + + const details = await controller.getDetails(1); + + expect(details).to.containDeep({name: 'Pen'}); + expect(findStub).to.be.calledWithMatch({where: {id: 1}}); + }); + }); + + function givenStubbedRepository() { + repository = sinon.createStubInstance(ProductRepository); + } +}); +``` + +### Unit test your models and repositories + +In a typical LoopBack application, models and repositories rely on behavior provided by the framework (`@loopback/repository` package) and there is no need to test LoopBack's built-in functionality. However, any additional application-specific API does need new unit tests. + +For example, if the `Person` Model has properties `firstname`, `middlename` and `surname` and provides a function to obtain the full name, then you should write unit tests to verify the implementation of this additional method. + +Remember to use [Test data builders](#use-test-data-builders) whenever you need valid data to create a new model instance. + +{% include code-caption.html content="test/unit/models/person.model.unit.ts" %} + +```ts +import {Person} from '../../models/person.model' +import {givenPersonData} from '../helpers/database.helpers' +import {expect} from '@loopback/testlab'; + +describe('Person (unit)', () => { + // we recommend to group tests by method names + describe('getFullName()', () => { + it('uses all three parts when present', () => { + const person = givenPerson({ + firstname: 'Jane', + middlename: 'Smith', + surname: 'Brown' + })); + + const fullName = person.getFullName(); + expect(fullName).to.equal('Jane Smith Brown'); + }); + + it('omits middlename when not present', () => { + const person = givenPerson({ + firstname: 'Mark', + surname: 'Twain' + })); + + const fullName = person.getFullName(); + expect(fullName).to.equal('Mark Twain'); + }); + }); + + function givenPerson(data: Partial) { + return new Person(givenPersonData(data)); + } +}); +``` + +Writing a unit test for a custom repository methods is not straightforward because `CrudRepositoryImpl` is based on legacy loopback-datasource-juggler that was not designed with dependency injection in mind. Instead, use integration tests to verify the implementation of custom repository methods; see [Test your repositories against a real database](#test-your-repositories-against-a-real-database) in [Integration Testing](#integration-testing). + +### Unit test your Sequence + +While it's possible to test a custom Sequence class in isolation, it's better to rely on acceptance-level tests in this exceptional case. The reason is that a custom Sequence class typically has many dependencies (which makes test setup too long and complex), and at the same time it provides very little functionality on top of the injected sequence actions. Bugs are much more likely to caused by the way how the real sequence action implementations interact together (which is not covered by unit tests), instead of the Sequence code itself (which is the only thing covered). + +See [Test Sequence customizations](#test-sequence-customizations) in [Acceptance Testing](#acceptance-testing). + +### Unit test your Services + +{% include content/tbd.html %} + +To be done. The initial beta release does not include Services as a first-class feature. + +See the following related GitHub issues: + + - Define services to represent interactions with REST APIs, SOAP Web Services, gRPC services, and more: [#522](https://github.com/strongloop/loopback-next/issues/522) + - Guide: Services [#451](https://github.com/strongloop/loopback-next/issues/451) + +## Integration testing + +Integration tests are considered "white-box" tests because they use an "inside-out" approach that tests how multiple units work together or with external services. You can use test doubles to isolate tested units from external variables/state that are not part of the tested scenario. + +### Test your repositories against a real database + +There are two common reasons for adding repository tests: + - Your models are using advanced configuration, for example, custom column mappings, and you want to verify this configuration is correctly picked up by the framework. + - Your repositories have additional methods. + +Integration tests are one of the places to put the best practices in [Data handling](#data-handling) to work: + + - Clean the database before each test + - Use test data builders + - Avoid sharing the same data for multiple tests + +Here is an example showing how to write an integration test for a custom repository method `findByName`: + +{% include code-caption.html content= "tests/integration/repositories/category.repository.integration.ts" %} +```ts +import {givenEmptyDatabase} from '../../helpers/database.helpers.ts'; + +describe('CategoryRepository (integration)', () => { + beforeEach(givenEmptyDatabase); + + describe('findByName(name)', () => { + it('return the correct category', async () => { + const stationery = await givenCategory({name: 'Stationery'}); + const groceries = await givenCategory({name: 'Groceries'}); + const repository = new CategoryRepository(); + + const found = await repository.findByName('Stationery'); + + expect(found).to.deepEqual(stationery); + }); + }); +}); +``` + +### Test Controllers and repositories together + +Integration tests running controllers with real repositories are important to verify that the controllers use the repository API correctly, and the commands and queries produce expected results when executed on a real database. These tests are similar to repository tests: we are just adding controllers as another ingredient. + +```ts +import {ProductController, ProductRepository, Product} from '../..'; +import {expect} from '@loopback/testlab'; +import {givenEmptyDatabase, givenProduct} from '../helpers/database.helpers'; + +describe('ProductController (integration)', () => { + beforeEach(givenEmptyDatabase); + + describe('getDetails()', () => { + it('retrieves details of the given product', async () => { + const inkPen = await givenProduct({name: 'Pen', slug: 'pen'}); + const pencil = await givenProduct({name: 'Pencil', slug: 'pencil'}); + const controller = new ProductController(new ProductRepository()); + + const details = await controller.getDetails('pen'); + + expect(details).to.eql(pencil); + }); + }); +}); +``` + +### Test your Services against real backends + +{% include content/tbd.html %} + +To be done. The initial beta release does not include Services as a first-class feature. + +## Acceptance (end-to-end) testing + +Automated acceptance (end-to-end) tests are considered "black-box" tests because they use an "outside-in" approach that is not concerned about the internals of the system, just simply do the same actions (send the same HTTP requests) as the clients and consumers of your API will do, and verify the results returned by the system under test are matching the expectations. + +Typically, acceptance tests start the application, make HTTP requests to the server, and verify the returned response. LoopBack uses [supertest](https://github.com/visionmedia/supertest) to make the test code that executes HTTP requests and verifies responses easier to write and read. +Remember to follow the best practices from [Data handling](#data-handling) when setting up your database for tests: + + - Clean the database before each test + - Use test data builders + - Avoid sharing the same data for multiple tests + +### Validate your OpenAPI specification + +The OpenAPI specification is a cornerstone of applications that provide REST APIs. +It enables API consumers to leverage a whole ecosystem of related tooling. To make the spec useful, you must ensure it's a valid OpenAPI Spec document, ideally in an automated way that's an integral part of regular CI builds. LoopBack's [testlab](https://www.npmjs.com/package/@loopback/testlab) module provides a helper method `validateApiSpec` that builds on top of the popular [swagger-parser](https://www.npmjs.com/package/swagger-parser) package. + +Example usage: + +```ts +// test/acceptance/api-spec.acceptance.ts +import {validateApiSpec} from '@loopback/testlab'; +import {HelloWorldApp} from '../..'; +import {RestServer} from '@loopback/rest'; + +describe('API specification', () => { + it('api spec is valid', async () => { + const app = new HelloWorldApp(); + const server = await app.getServer(RestServer); + const spec = server.getApiSpec(); + await validateApiSpec(apiSpec); + }); +}); +``` + +### Perform an auto-generated smoke test of your REST API + +The formal validity of your application's spec does not guarantee that your implementation is actually matching the specified behavior. To keep your spec in sync with your implementation, you should use an automated tool like [Dredd](https://www.npmjs.com/package/dredd) to run a set of smoke tests to verify conformance of your app with the spec. + +Automated testing tools usually require little hints in your specification to tell them how to create valid requests or what response data to expect. Dredd in particular relies on response [examples](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#exampleObject) and request parameter [x-example](http://dredd.org/en/latest/how-to-guides.html#example-values-for-request-parameters) fields. Extending your API spec with examples is good thing on its own, since developers consuming your API will find them useful too. + +Here is an example showing how to run Dredd to test your API against the spec: + +{% include code-caption.html content= " " %} +```ts +describe('API (acceptance)', () => { + let dredd: any; + before(initEnvironment); + + it('conforms to the specification', done => { + dredd.run((err: Error, stats: object) => { + if (err) return done(err); + expect(stats).to.containDeep({ + failures: 0, + errors: 0, + skipped: 0, + }); + done(); + }); + }); + + async function initEnvironment() { + const app = new HelloWorldApp(); + const server = app.getServer(RestServer); + // For testing, we'll let the OS pick an available port by setting + // RestBindings.PORT to 0. + server.bind(RestBindings.PORT).to(0); + // app.start() starts up the HTTP server and binds the acquired port + // number to RestBindings.PORT. + await app.start(); + // Get the real port number. + const port = await server.get(RestBindings.PORT); + const baseUrl = `http://localhost:${port}`; + const config: object = { + server: baseUrl, // base path to the end points + options: { + level: 'fail', // report 'fail' case only + silent: false, // false for helpful debugging info + path: [`${baseUrl}/swagger.json`], // to download apiSpec from the service + } + }; + dredd = new Dredd(config); + }); +}) +``` + +The user experience is not as great as we would like it, we are looking into better solutions; see [GitHub issue #644](https://github.com/strongloop/loopback-next/issues/644). Let us know if you can recommend one! + +### Test your individual REST API endpoints + +You should have at least one acceptance (end-to-end) test for each of your REST API endpoints. Consider adding more tests if your endpoint depends on (custom) sequence actions to modify the behavior when the corresponding controller method is invoked via REST, compared to behavior observed when the controller method is invoked directly via JavaScript/TypeScript API. For example, if your endpoint returns different response to regular users and to admin users, then you should have two tests: one test for each user role. + +Here is an example of an acceptance test: + +```ts +// test/acceptance/product.acceptance.ts +import {HelloWorldApp} from '../..'; +import {RestBindings, RestServer} from '@loopback/rest'; +import {expect, supertest} from '@loopback/testlab'; +import {givenEmptyDatabase, givenProduct} from '../helpers/database.helpers'; + +describe('Product (acceptance)', () => { + let app: HelloWorldApp; + let request: supertest.SuperTest; + + before(givenEmptyDatabase); + before(givenRunningApp); + + it('retrieves product details', async () => { + // arrange + const product = await givenProduct({ + name: 'Ink Pen', + slug: 'ink-pen', + price: 1, + category: 'Stationery', + description: 'The ultimate ink-powered pen for daily writing', + label: 'popular', + available: true, + endDate: null, + }); + + // act + const response = await request.get('/product/ink-pen') + + // assert + expect(response.body).to.deepEqual({ + id: product.id, + name: 'Ink Pen', + slug: 'ink-pen', + price: 1, + category: 'Stationery', + available: true, + description: 'The ultimate ink-powered pen for daily writing', + label: 'popular', + endDate: null, + }); + }); + + async function givenRunningApp() { + app = new HelloWorldApp(); + const server = await app.getServer(RestServer); + server.bind(RestBindings.PORT).to(0); + await app.start(); + + const port: number = await server.get(RestBindings.PORT); + request = supertest(`http://127.0.0.1:${port}`); + } +}); +``` + +### Test Sequence customizations + +Custom sequence behavior is best tested by observing changes in behavior of affected endpoints. For example, if your sequence has an authentication step that rejects anonymous requests for certain endpoints, then you can write a test making an anonymous request to such an endpoint to verify that it's correctly rejected. These tests are essentially the same as the tests verifying implementation of individual endpoints as described in the previous section. diff --git a/docs/Using-components.md b/docs/Using-components.md new file mode 100644 index 000000000000..96ffeadb900a --- /dev/null +++ b/docs/Using-components.md @@ -0,0 +1,30 @@ +--- +lang: en +title: 'Using components' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Using-components.html +summary: +--- +Components are an important part of LoopBack 4. We are keeping the core small and easy to extend, making it easy for independent developers to contribute additional features to LoopBack. Components are the vehicle for bringing more goodness to your applications. + +A typical LoopBack component is an [npm](https://www.npmjs.com/) package exporting a Component class which can be added to your application. + +```js +import {Application} from '@loopback/core'; +import {AuthenticationComponent} from '@loopback/authentication'; + +const app = new Application(); +app.component(AuthenticationComponent); +``` + +In general, components can contribute the following items: + + - [Controllers](Controllers.md) + - Providers of additional [Context values](Context.md) + +In the future (before the GA release), components will be able to contribute additional items: + + - Models + - [Repositories](Repositories.md) diff --git a/docs/Using-decorators.md b/docs/Using-decorators.md new file mode 100644 index 000000000000..c1a5a79b8a3a --- /dev/null +++ b/docs/Using-decorators.md @@ -0,0 +1,22 @@ +--- +lang: en +title: 'Using decorators' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Using-decorators.html +summary: +--- + +{% include content/tbd.html %} + +> This will describe how to use existing decorators to build apps per [#552](https://github.com/strongloop/loopback-next/issues/552) + +Potential topics: +- What is a decorator? Allows you annotate your classes with metadata. + - simple example `@authenticate` +- How to use decorators (what are the features) + - Define routes (@api) + - Define access controls + - Compose and customize decorators +- Note: you have to use Babel or TypeScript for decorator support diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000000..8ac5b90f51bd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,35 @@ +--- +lang: en +title: LoopBack 4 +toc: false +keywords: LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/index.html +summary: LoopBack is a platform for building APIs and microservices in Node.js +--- + +{% include important.html content="LoopBack 4 is the next step in the evolution of LoopBack. It is still in early development and is not yet released. +" %} + +{% include see-also.html title="GitHub Repo" content=' +LoopBack 4 framework code is being developed in one "mono-repository", +[loopback-next](https://github.com/strongloop/loopback-next), rather than multiple repos, as in v3. However, examples and externally-developed components will be in separate repositories. +'%} +## Built for API developers + + - Define your API endpoints and schemas using the [OpenAPI](https://www.openapis.org/) standard. + - Write your endpoints in modern JavaScript using ES2017, `async` / `await`, modules. + - Use your defined endpoints and schemas as the source of truth without generating code. + +## Built for teams + + - Review changes to API endpoints without digging through JavaScript. + - Maintain consistency by automating the validation of your endpoints and schemas. + - First class support for [TypeScript](https://www.typescriptlang.org) (strongly typed JavaScript). + +## Built for your platform + + - Use LoopBack as a starting point for your own framework or platform. + - Build libraries of reusable components in a standardized way. + - Integrate with databases, web services, and other platforms using connectors.