Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update LoopBack 4 docs with rest refactor changes #475

Merged
merged 14 commits into from
Oct 10, 2017
84 changes: 59 additions & 25 deletions pages/en/lb4/Context.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,73 @@ summary:

- An abstraction of all state and dependencies in your application.
- Context is what LB uses to "manage" everything.
- A global registry for anything/everything in your app (all configs, state, dependencies, classes, etc).
- A global registry for anything/everything in your app (all configs, state,
dependencies, classes, etc).
- An IoC container used to inject dependencies into your code.

Why is it important to you?

- 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.html) and decorators).
- You have full access to updated/real-time application+request state at all times.
- 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.html) and decorators).
- You have full access to updated/real-time application+request state at all
times.

LoopBack supports two types of context: Application-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)
- 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;
const app = new Application(); // `app` is a "Context"
class MySequence { ... }
app.sequence(MySequence);
class MyController { ... }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we revert this since either can be used as an example, but in the Request-level context section we expand on MySequence?

app.controller(MyController);
```

In this case, you are using our sequence sugar/helper to register a new default sequence. The important point to note is `MySequence` is actually registered into the Application Context (`app` is a Context).
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).

## Request-level context (request)

- dynamically created for each incoming server request
- extends the application level context (to give you access to application level dependencies during the request/response lifecycle)
- is garbage collected once the response is sent (memory management)
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)
const req = this.ctx.getSync('http.request'); // but it is still available in the sequence/request 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
- 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:
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
Expand All @@ -71,18 +87,26 @@ 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)
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).
The process of registering a ContextValue into the Context is known as
_binding_. Sequence-level bindings work the same way (shown 2 examples before).

## 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.
- 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.
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 contxet via the `@inject` decorator:
However, when using classes, LoopBack provides a better way to get at stuff in
the context via the `@inject` decorator:

```js
const Application = require('@loopback/core');
Expand All @@ -100,13 +124,21 @@ class HelloController {
}
```

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.
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.html) for further details.
Please refer to [Dependency injection](Dependency-injection.html) 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.
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):

```js
class HelloController {
Expand All @@ -121,4 +153,6 @@ class HelloController {
}
```

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.
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.
18 changes: 12 additions & 6 deletions pages/en/lb4/Dependency-Injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ In LoopBack, we use [Context](Context.html) to keep track of all injectable depe

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:

```js
```ts
// TypeScript example

import {BasicStrategy} from 'passport-http';
import {Application} from '@loopback/core';
import {RestServer} from '@loopback/rest';
// basic scaffolding stuff happens in between...

app.bind('authentication.strategy').to(new BasicStrategy(loginUser));
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
Expand All @@ -55,12 +61,12 @@ function loginUser(username, password, cb) {

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:

```js
app.bind('authentication.provider').toClass(AuthenticationProvider);
```ts
server.bind('authentication.provider').toClass(AuthenticationProvider);

const provider = await app.get('authentication.provider');
const provider = await server.get('authentication.provider');
// provider is an AuthenticationProvider instance
// provider.strategy was set to the value returned by app.get('authentication.strategy')
// provider.strategy was set to the value returned by server.get('authentication.strategy')
```

When a binding is created via `.toClass()`, [Context](Context.html) 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.
Expand Down
18 changes: 12 additions & 6 deletions pages/en/lb4/Getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,27 @@ The starting point of this flavor of JavaScript is [ECMAScript 6](http://www.ecm

Here is the most basic LoopBack.next application:

```js
```ts
import {Application} from '@loopback/core';
import {RestComponent, RestServer} from '@loopback/rest';

const app = new Application();
app.handler((sequence, request, response) => {
sequence.send(response, 'hello world');
const app = new Application({
components: [RestComponent],
});

(async function start() {
// Grab the REST server instance
const server = await app.getServer(RestServer);
// Setup our handler!
server.handler((sequence, request, response) => {
sequence.send(response, 'hello world');
});
await app.start();
console.log(`The app is running on port ${app.getSync('http.port')}`);
console.log(`REST server listening on port ${server.getSync('rest.port')}`);
})();
```

The example above creates an `Application` that responds to all HTTP requests with the text "Hello World".
The example above creates an `Application` and a `RestServer` that responds to all HTTP requests with the text "Hello World".

To see what the complete application looks like, see [loopback-next-hello-world](https://github.com/strongloop/loopback-next-hello-world/).

Expand Down
47 changes: 24 additions & 23 deletions pages/en/lb4/Reserved-binding-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,33 @@ import { CoreBindings } from '@loopback/authentication'

### Binding keys

**Sequence Actions 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 Sequence Actions CONSTANT's, bind/inject to `CoreBindings.SequenceActions.CONSTANT` *OR*
To use the Rest Sequence Actions CONSTANTs, bind/inject to `RestBindings.SequenceActions.CONSTANT` *OR*

```js
const SequenceActions = CoreBindings.SequenceActions;
const SequenceActions = RestBindings.SequenceActions;
SequenceActions.CONSTANT // CONSTANT to bind/inject
```

Expand All @@ -106,26 +127,6 @@ SequenceActions.CONSTANT // CONSTANT to bind/inject
|`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|

**Other 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`|
|`http.handler`|`HTTP_HANDLER`|`HttpHandler`|The HTTP Request Handler|
|`http.port`|`HTTP_PORT`|`number`|HTTP Port the application will run on|
|`http.request`|`Http.REQUEST`|`ServerRequest`|The raw `http` request object|
|`http.request.context`|`Http.CONTEXT`|`Context`|Request level context|
|`http.response`|`Http.RESPONSE`|`ServerResponse`|The raw `http` response object|
|`routes.${route.verb}.${route.path}`||`RouteEntry`|Route entry specified in api-spec|
|`sequence`|`SEQUENCE`|`SequenceHandler`|Class that implements the sequence for your application|

## Package: openapi-spec

**Reserved prefixes:**
Expand Down
24 changes: 17 additions & 7 deletions pages/en/lb4/Routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ summary:

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.html).
Expand All @@ -32,10 +35,11 @@ In the example above, `name` is a Parameter. Parameters are values, usually pars
- `header`
- `path` (url)

## Creating Routes
## Creating REST Routes

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.
```js
const app = new Application();

Expand All @@ -48,17 +52,23 @@ const spec = {
}
}
};
const route = new Route('get', '/', spec, greet);
app.route(route);

app.start();
(async function start() {
const server = await app.getServer(RestServer);
const route = new Route('get', '/', spec, greet);
server.route(route);
await app.start();
})();
Copy link
Member

@bajtos bajtos Oct 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will swallow any errors thrown from start, causing "unhandled promise rejection" warnings. You should add .catch() handler at minimum.

It makes me wonder, can we move methods like route, handler and sequence to a different class, one that does not have any dependencies so that it will always resolve synchronously, and then inject this class as a RestServer dependency?

An mock-up:

// inside RestComponent (or a mixin?)
app.rest = new RestServerConfigurator();
app.bind(RestBindings.CONFIGURATOR).toDynamicValue(() => app.rest);

// in RestServer constructor
class RestServer {
  constructor(
    @inject(RestBindings.CONFIGURATOR) configurator: RestServerConfigurator,
    /*...*/) {
      // define routes, handler, sequence etc. from the configurator
    }
 }

// usage in applications
app.rest.route('get', '/', spec, greet);

This can be extended to support multiple rest servers by replacing app.rest with app.${serverName} , where the server name is the key used in servers.${serverName} binding and in appConfig.servers object.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave this out of scope of this documentation update.


```

## Declaring Routes with API specifications
## 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: {
Expand All @@ -77,7 +87,7 @@ const spec = {
}
};

app.api(spec);
server.api(spec);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very wrong to me. The API is tightly coupled with Controllers and controllers are defined at application level. As a user, I'd expect api() method to be available in the same place as controller().

Also, if the API is server-specific, and we have multiple REST servers configured, then each server can be providing a different REST API. Is it a good idea?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very wrong to me. The API is tightly coupled with Controllers and controllers are defined at application level. As a user, I'd expect api() method to be available in the same place as controller().

My understanding was that we might have multiple controllers servicing more than one server, either of the same protocol or otherwise. Combine this with the idea that the OpenAPI spec handling is REST-specific and we end up with server-based API binding instead.

Also, if the API is server-specific, and we have multiple REST servers configured, then each server can be providing a different REST API. Is it a good idea?

I think it's a great idea! Your microservice could have a public-facing API that defines its consumable endpoints, and a private/internal API for infrastructure, exporting logs, administration, or whatever you can think of. Without server-level API binding, these scenarios aren't possible anymore.

Playing "Devil's Advocate" for a moment, if we want to strictly enforce the idea that a LoopBack application is a one-server microservice, then it would make sense to migrate some of this configuration back up to application level, and enforce the idea that you pick one, and only one server component to power the application. This would definitely make sense from a microservice architecture perspective, where you're minimizing the responsibility of each running instance and using other technologies to handle communication between services, log aggregation, etc.

I think trying to simultaneously support both philosophies is easily doable, but could be a documentation and UX nightmare for new users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your microservice could have a public-facing API that defines its consumable endpoints, and a private/internal API for infrastructure, exporting logs, administration, or whatever you can think of.

This is a pretty cool idea which makes a lot of sense to me 👍 I agree we should support this use case.

My concern is about ease of use for people starting with the framework, notice how many of our examples increased in size.

Anyways, this is out of scope of this pull request, because we have already updated may other places (package READMEs, example repos) to use the convention described in this pull request. To use a better convention, we will need to update both the docs and those other places.

```

## Invoking operations using Routes
Expand Down
Loading