-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(rest): provide more options to customize api explorer and openapi spec #1637
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
Conversation
|
What's wrong with using a path only in the server url ( Please note that OpenAPI spec allows relative URLs like the one we are using. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#serverObject
I am personally against landing this change and prefer to look into ways how to fix OpenAPI consumers to correctly support relative URLs. |
|
@bajtos Good point. Let's evaluate a bit more. |
|
From what I remember the OASGraph team said this was not a critical fix unlike the |
|
Can we perhaps make the server URL configurable?
Thoughts? |
+1.
+1.
|
9dcd3a6 to
c78d636
Compare
01258fb to
944b06e
Compare
|
@bajtos PTAL |
packages/rest/README.md
Outdated
| setServersFromRequest: false, | ||
| // Template the OpenAPI spec, which will be filled with `paths` from | ||
| // controllers. | ||
| openApiSpec: { |
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 base template for OpenAPI spec should not be set in the apiExplorer object. It should be it's own top level property under rest.
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.
Good point. Fixed.
944b06e to
8f3c135
Compare
packages/rest/README.md
Outdated
| | ----------- | ------------------ | --------------------------------------------------------------------------------------------------------- | | ||
| | port | number | Specify the port on which the RestServer will listen for traffic. | | ||
| | sequence | SequenceHandler | Use a custom SequenceHandler to change the behavior of the RestServer for the request-response lifecycle. | | ||
| | apiExplorer | ApiExplorerOptions | Custom how API explorer and OpenAPI spec is served | |
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.
Please document openApiSpec property in this table. :)
packages/rest/README.md
Outdated
| // default to https://loopback.io/api-explorer | ||
| explorerUrl: 'http://petstore.swagger.io', | ||
| // Set `servers` based on HTTP request headers, default to `false` | ||
| setServersFromRequest: false, |
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 don't think this is a apiExplorer related property either but now sure what a better place for this might be.
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.
How about moving this property from rest.apiExplorer.setServersFromRequest to something like rest.updateOpenApiServersFromRequest?
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.
Uh, rest.updateOpenApiServersFromRequest is awfully long. How about rest.setServerUrlsFromRequest?
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.
Or how about just rest.serverUrlsFromRequest?
8f3c135 to
41b15e6
Compare
bajtos
left a comment
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.
The changes look much better now, let's discuss the details bit more.
packages/rest/README.md
Outdated
| // default to https://loopback.io/api-explorer | ||
| explorerUrl: 'http://petstore.swagger.io', | ||
| // Set `servers` based on HTTP request headers, default to `false` | ||
| setServersFromRequest: false, |
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.
How about moving this property from rest.apiExplorer.setServersFromRequest to something like rest.updateOpenApiServersFromRequest?
packages/rest/README.md
Outdated
|
|
||
| // Template of the OpenAPI spec, which will be filled with `paths` from | ||
| // controllers. | ||
| openApiSpec: { |
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 believe we already have an API for setting the initial OpenAPI spec values, see http://apidocs.loopback.io/@loopback%2fdocs/rest.html#RestApplication.prototype.api
Can we keep using app.api() instead of introducing this new option? If not, then what is your reasoning? What benefits do you see in this declarative approach?
The current downside of app.api() is that the user has to provide spec.openapi and spec.paths properties in addition to info.
app.api({
openapi: '3.0.1',
info: { /*...*/ },
servers: [/*...*/],
paths: {},
});On the brighter side, app.api() allows TypeScript to verify the correctness of the passed values. I am not sure if the type checks work for your declarative approach, because the type used for options allow arbitrary (additional) properties.
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 can relax the constraints on the argument of app.api() to allow any subset of OpenAPISpec and fill in openapi version and an empty paths object ourselves.
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.
See also #1637 (comment)
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.
Doesn't app.api() take in an entire spec vs. the new openApiSpec property which is intended to accept a base template for the spec rather than the complete spec itself (which would come from app.api)? I think the name openApiSpec doesn't indicate clearly that it's intended to be just a template.
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 believe our current code merges the following sources:
- the spec provided by
app.api()(if any) - specs provided at controller level via
@apidecorator, - specs built from controller-method-level decorators like
@getand@param - specs provided via
app.route(new Route(verb, path, 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.
I don't mind providing a declarative way for providing a base template for the OpenAPI spec, perhaps in addition to app.api() API. However, I think we need to move that declarative config to a different place than application/server config, see #1637 (comment).
packages/rest/README.md
Outdated
| apiExplorer: { | ||
| // URL for the hosted API Explorer UI | ||
| // default to https://loopback.io/api-explorer | ||
| explorerUrl: 'http://petstore.swagger.io', |
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 use a shorter name url? The fact that this setting is controlling API Explorer is already encoded in the parent property name apiExplorer.
packages/rest/README.md
Outdated
| | ----------- | ------------------ | --------------------------------------------------------------------------------------------------------- | | ||
| | port | number | Specify the port on which the RestServer will listen for traffic. | | ||
| | sequence | SequenceHandler | Use a custom SequenceHandler to change the behavior of the RestServer for the request-response lifecycle. | | ||
| | openApiSpec | OpenApiSpec | Base template of the OpenAPI 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 is not entirely correct. OpenApiSpec type requires openapi version and paths object to be present.
packages/rest/src/rest.server.ts
Outdated
| */ | ||
| public requestHandler: HttpRequestListener; | ||
|
|
||
| protected _config: RestServerConfig; |
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.
IMO, this property should not be modified after the constructor has finished, let's mark it as readonly please.
Is there any particular reason for keeping the config protected? I mean can we make it public?
packages/rest/src/rest.server.ts
Outdated
| if (options.format === 'json') { | ||
| if ( | ||
| this._config.apiExplorer && | ||
| this._config.apiExplorer.setServersFromRequest |
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.
To avoid repetitive check for this._config.apiExplorer, I am proposing to always initialize this property in the constructor, so that it's either an empty object or the user-provider object. Then we can simplify this part as follows:
if (this._config.apiExplorer.setServersFromRequest)See also https://loopback.io/doc/en/contrib/style-guide.html#indentation-of-multi-line-expressions-in-if
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.
The style/formatting is enforced by prettier.
packages/rest/src/rest.server.ts
Outdated
| request, | ||
| options, | ||
| )}/openapi.json`; | ||
| (this._config.apiExplorer && this._config.apiExplorer.explorerUrl) || |
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.
To avoid repetitive check for this._config.apiExplorer, I am proposing to always initialize this property in the constructor, so that it's either an empty object or the user-provider object. Then we can simplify this part as follows:
const baseUrl = this._config.apiExplorer.explorerUrl || 'https://loopback.io/api-explorer';We can even set explorerUrl to the default value in the constructor, that would allow us to simplify this code even further.
| rest: { | ||
| port: 0, | ||
| }, | ||
| }); |
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 looks like a change of formatting only, can we revert it please? Unless there is a reason for this change, in which case I'd like to hear 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.
I use prettier to format the code
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 believe prettier allow multiple formatting styles these days, depending on how you write the code. I.e. it preserves one-liner {rest: {port: 0}} if you started with it, or keeps multi-line version if that's what the developer wrote.
| }, | ||
| }); | ||
| expect(response.get('Access-Control-Allow-Origin')).to.equal('*'); | ||
| expect(response.get('Access-Control-Allow-Credentials')).to.equal('true'); |
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.
IIUC, this test is verifying how rest.openApiSpec config is applied. There is no need to check CORS headers in such case, they have been already verified by previous tests.
Please remove these two assertions.
| }); | ||
| expect(response.body.servers[0].url).to.match(/http:\/\/127.0.0.1\:\d+/); | ||
| expect(response.get('Access-Control-Allow-Origin')).to.equal('*'); | ||
| expect(response.get('Access-Control-Allow-Credentials')).to.equal('true'); |
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.
Ditto.
IIUC, this test is verifying how rest.openApiSpec config is applied. There is no need to check CORS headers in such case, they have been already verified by previous tests.
Please remove these two assertions.
|
On the second thought, my main objection against providing OpenAPI spec in This something we have already discussed in the past, see #742.
In this light, if we want to make OpenAPI |
d203959 to
bcd609a
Compare
|
@bajtos I made some changes based on your feedback. The |
bajtos
left a comment
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.
Thank you for addressing my comments, the proposal looks so much better now!
I am not convinced that the info object is something that application consumers should be allowed to change. For example, isn't info.version always controlled by the application?
My proposal: allow users to configure server arrays only. (Remove rest.openApiSpec.template, add rest.openApiSpec.servers).
Thoughts?
| rest: { | ||
| port: 0, | ||
| }, | ||
| }); |
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 believe prettier allow multiple formatting styles these days, depending on how you write the code. I.e. it preserves one-liner {rest: {port: 0}} if you started with it, or keeps multi-line version if that's what the developer wrote.
packages/rest/src/rest.server.ts
Outdated
| options, | ||
| )}/openapi.json`; | ||
| (this.config.apiExplorer && this.config.apiExplorer.url) || | ||
| 'https://loopback.io/api-explorer'; |
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 you set the default value for apiExplorer.url inside RestServer constructor please? Not only it will simplify the code here, but it will make it easier for 3rd parties to introspect the API Explorer configuration and obtain the actual URL used even when no custom value was configured by the user.
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.
Fixed
packages/rest/README.md
Outdated
| rest: { | ||
| port: 3001, | ||
|
|
||
| // Template of the OpenAPI spec, which will be filled with `paths` from |
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 proposing to move this new documentation to https://github.com/strongloop/loopback-next/blob/master/docs/site/Server.md and modify this README with a link pointing to that doc page.
For example, configuration of HTTPS is already described in Server.md only, see here.
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 consolidate the README.md with Server.md in a separate PR.
bcd609a to
41b4bfd
Compare
|
@bajtos PTAL |
41b4bfd to
6a78b9f
Compare
7fe7531 to
d58c107
Compare
packages/rest/src/rest.server.ts
Outdated
| * security imposed by browsers as the spec is exposed over `http` by default. | ||
| * https://github.com/strongloop/loopback-next/issues/1603 | ||
| */ | ||
| urlForHttp?: string; |
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 would expect LoopBack to be smart enough to figure out that urlForHttp is the same as url, but using http:// instead of https://.
What are the use cases you have in mind where urlForHttp would significantly differ from url?
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.
urlForHttp is optional and we can infer it from url in most cases. I added it there since we logically need two values and have to store both of them anyway.
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.
How about httpUrl?
packages/rest/README.md
Outdated
| port: 3001, | ||
|
|
||
| // Optional configuration for how to serve OpenAPI spec | ||
| openApiSpec: { |
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.
IMO, this configuration example has too many comments to make it practical. That's why I proposed to move this content to https://github.com/strongloop/loopback-next/blob/master/docs/site/Server.md, where you can split the code into multiple sections/paragraph and use regular text instead of code comments.
I am fine with your proposal to leave consolidation of README out of scope of this pull request.
What I would like to ask you is to move this new README content to Server.md right now, so that we don't introduce more work for the future consolidation effort; plus reformat the documentation of config options into a form that's easier to read. It would be great to give a wider context to the readers while you are at this, e.g. why and when to change openApiSpec configuration, etc.
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'll clean up the docs.
b-admike
left a comment
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 have one comment, otherwise LGTM 👍
| }); | ||
|
|
||
| it('exposes "GET /swagger-ui" endpoint with apiExplorer.urlForHttp', async () => { | ||
| const server = await givenAServer({ |
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.
Sorry maybe I'm missing something, but where are the assertions for this test case?
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.
See below:
const expectedUrl = new RegExp(
[
'http://petstore.swagger.io',
'\\?url=http://\\d+.\\d+.\\d+.\\d+:\\d+/openapi.json',
].join(''),
);
expect(response.get('Location')).match(expectedUrl);
The url is now 'http://petstore.swagger.io
828295d to
154f434
Compare
|
@bajtos Moved REST server configuration to |
packages/rest/src/rest.server.ts
Outdated
| // spec to be converted into an XML response. | ||
| const settings = OPENAPI_SPEC_MAPPING[request.url]; | ||
| return this._serveOpenApiSpec(request, response, settings); | ||
| const form = mapping[request.url]; |
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.
Why is this called form? Seems unintuitive.
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.
What's a better name for version + format?
packages/rest/src/rest.server.ts
Outdated
| config.apiExplorer.url = | ||
| config.apiExplorer.url || 'https://loopback.io/api-explorer'; | ||
|
|
||
| config.apiExplorer.urlForHttp = |
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 httpUrl might be a better name.
packages/rest/src/rest.server.ts
Outdated
| * security imposed by browsers as the spec is exposed over `http` by default. | ||
| * https://github.com/strongloop/loopback-next/issues/1603 | ||
| */ | ||
| urlForHttp?: string; |
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.
How about httpUrl?
154f434 to
d2572f3
Compare
virkt25
left a comment
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.
Looks great!
packages/rest/src/rest.server.ts
Outdated
| // spec to be converted into an XML response. | ||
| const settings = OPENAPI_SPEC_MAPPING[request.url]; | ||
| return this._serveOpenApiSpec(request, response, settings); | ||
| const specForm = mapping[request.url]; |
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.
Great name!
bajtos
left a comment
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.
The documentation looks so much better now 👍
Let's clean up the test code a bit, see my comments below.
Additionally, please a test to verify how we treat IPv6 addresses, see #1623
| version: '1.0.0', | ||
| }, | ||
| servers: [{url: 'http://127.0.0.1:8080'}], | ||
| paths: { |
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.
Please simplify this assertion to focus only on the part that's important (i.e. servers property) and leave out the rest of the API spec that's only adding noise to error messages and test source code.
expect(response.body.servers).to.deepEqual([
{url: 'http://127.0.0.1:8080'},
]);| }, | ||
| }); | ||
| const greetSpec = { | ||
| responses: { |
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 OpenAPI snippet is repeated in many tests. Please extract it into a helper function, e.g a function that adds a dummy route.~~~
Do we really need a route to verify how top-level spec properties are handled? Isn't in enough to render the spec for an empty app? Such app should still produce servers property of the spec. See the test case "exposes endpoints with openApiSpec.endpointMapping" for an example.
Leaving out the route we don't need in the test will make the test code much simpler and easier to understand. Please remove it.
| }, | ||
| }, | ||
| }; | ||
| server.route(new Route('get', '/greet', greetSpec, function greet() {})); |
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 believe this route is not needed by this test, could you please remove?
| }, | ||
| }, | ||
| }; | ||
| server.route(new Route('get', '/greet', greetSpec, function greet() {})); |
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 believe this route is not needed to verify that "setServersFromRequest" is handled correctly, could you please remove it?
d2572f3 to
b56fb37
Compare
b56fb37 to
eca79a4
Compare
|
@bajtos Your comments are addressed:
PTAL. |
| config.apiExplorer.url = | ||
| config.apiExplorer.url || 'https://loopback.io/api-explorer'; | ||
|
|
||
| config.apiExplorer.httpUrl = |
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 come up with a more intuitive interface/names for apiExplorer.url and apiExplorer.httpUrl?
httpUrl makes me wonder if there would be httpsUrl as well.
I know it is not trivial and might get overly complicated for a simple thing, but I'd like the ability to set a HTTP url and HTTPS url, and mark one as the default.
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.
On second thoughts, I am fine with this. HTTPS is starting to become the expected default in browsers, let HTTPS be the default apiExplorer.url.
bajtos
left a comment
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
The generated openapi spec has a dummy server url at the moment:
servers: [url: '/']. The PR adds more options to rest server config.See https://github.com/strongloop/loopback-next/blob/c78d636b9cccf66bc843ec49b203b0a6c319f78e/packages/rest/README.md#configuration
Checklist
npm testpasses on your machinepackages/cliwere updatedexamples/*were updated