-
Notifications
You must be signed in to change notification settings - Fork 386
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
Conversation
Here is the swagger.json that will hook up your `GreetController` to the | ||
`/greet` path, and associate the `greet` function with a `GET` request. | ||
|
||
{% include code-caption.html content="src/apidef/swagger.json".js" %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Liquid syntax error (spurious quote). S/be:
{% include code-caption.html content="src/apidef/swagger.json" %}
I am going to rewrite that page as part of loopbackio/loopback-next#553 anyways. Thanks for updating the code examples though! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am concerned that the recent changes in server-related API worsened user experience and made the application setup code too complex.
I am proposing to take another look at the current implementation and find a way how to make the setup easier to use, to ensure we keeping the high UX level which we started with.
pages/en/lb4/Context.md
Outdated
|
||
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 | ||
const req = this.ctx.getSync('rest.http.request'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to not teach new developers about getSync
, because it does not work for values requiring async computation. I am proposing to update this example as follows:
class MySequence extends DefaultSequence {
// we provide request & response for convenience (taken from the Context)
async handle(request, response) {
// 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}`);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry, I must've glossed over that bit, was just correcting the injection key.
pages/en/lb4/Dependency-Injection.md
Outdated
|
||
app.bind('authentication.strategy').to(new BasicStrategy(loginUser)); | ||
const server = app.getServer(RestServer); // The REST server has its own context! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getServer
returns a promise, doesn't it?
const server = await app.getServer(RestServer);
pages/en/lb4/Dependency-Injection.md
Outdated
|
||
app.bind('authentication.strategy').to(new BasicStrategy(loginUser)); | ||
const server = app.getServer(RestServer); // The REST server has its own context! | ||
server.bind('authentication.strategy').to(new BasicStrategy(loginUser)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel the necessity to configure things like authentication strategy and sequence handler at server-level (and not at app-level) is rather suboptimal UX. What used to be a single line of sync code before, becomes two lines of async/await code that cannot even happen in application's constructor (because it's async).
Can we come up with a better approach that's easier to use? I am fine to leave that out of scope of this patch if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could easily have the examples use application-level context to do their work, since it's all available at the child context of the server, but then we're potentially encouraging a pattern that might prevent use of separate strategies in multiple server instances.
If we're going to alter the examples to simplify the bindings they work with, we'll need to make a section somewhere that details more advanced usage scenarios so that users properly separate their contextual bindings.
pages/en/lb4/Getting-started.md
Outdated
}); | ||
|
||
(async function start() { | ||
// Grab the REST server instance | ||
const server = app.getServer(RestServer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getServer
returns a promise, doesn't it?
I also find it weird that we are configuring the sequence handler every time the server is started, and that there is no sequence handler configured after application's constructor was called. (I think this is sort of an addition to my previous comment.)
|`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| | ||
|
||
**Sequence Actions binding keys** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this title should include "REST" keyword, now that the sequence actions listed below are specific to RestBindings
.
const route = new Route('get', '/', spec, greet); | ||
server.route(route); | ||
await app.start(); | ||
})(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@@ -77,7 +87,7 @@ const spec = { | |||
} | |||
}; | |||
|
|||
app.api(spec); | |||
server.api(spec); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
||
const app = new Application({ | ||
sequence: MySequence | ||
components: [RestComponent], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a step backwards in terms of UX, at least IMO. Isn't it possible to configure the sequence via component-specific configuration? Something along the following lines:
const app = new Application({
components: [RestComponent],
rest: {
sequence: MySequence,
}
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the current design supports your suggestion, so I'll change it.
|
||
I know this isn't a part of my PR, which is why I'm only leaving this comment | ||
instead of fixing it. | ||
--> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when I run npm start
on local, there is an error complains
Liquid Exception: Invalid syntax for include tag: content="src/apidef/swagger.json".js" Valid syntax: {% include file.ext param='value' param2='value' %} in pages/en/lb4/Testing-Your-application.md
jekyll 3.3.1 | Error: Invalid syntax for include tag:
content="src/apidef/swagger.json".js"
Valid syntax:
{% include file.ext param='value' param2='value' %}
branch gh-pages
doesn't fail on it.
This line is not directly modified in PR, maybe somewhere else affects it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops I commented in the wrong file that's why I didn't find it, should be
https://github.com/strongloop/loopback.io/pull/475/files#diff-ad241bc342976380fb64dab3b286802bR248
their names are so similar lol
|
||
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 { ... } |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Let's leave usage improvements that I suggested in my comments out of scope of this pull request.
I am going to land this pull request even if it's not perfect yet, so that I can start working on loopbackio/loopback-next#553 @kjdelisle please open a follow-up pull request to address remaining issues/comments. I apologize for the inconvenience this may have caused. |
Overview
Refactor the docs to cover off the changes made to the framework architecture
It's now a complete step-by-step set of working JavaScript-based examples that I've validated with my own repository: https://github.com/kjdelisle/hello-loopback/tree/doc-validation
Related Issues
loopbackio/loopback-next#613