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

feat: spike on API Explorer supporting complex objects in query param #4141

Closed
wants to merge 2 commits into from

Conversation

deepakrkris
Copy link
Contributor

@deepakrkris deepakrkris commented Nov 18, 2019

#3770

Scope:

Currently loopback supports both exploded and uri encoded complex objects in query params, even without the changes suggested in this PR. The purpose of this PR is to ONLY fix API Explorer to support complex objects in query params.

To test this spike,

  1. Install an example app (eg: todoList)
  2. change the param decorators for the filter query in a controller method to @param.query.jsonObject
  3. run npm build and copy the installed example app into this PR branch -> loopback-next/sandbox directory
  4. run npm i under loopback-next , to create symlinks
  5. cd sandbox/loopback4-example-todoList && npm run start
  6. goto API Explorer http://localhost:3000/explorer
  7. check invoking controller method that has @param.query.jsonObject works when a filter object is passed

@deepakrkris
Copy link
Contributor Author

Screen Shot 2019-11-18 at 9 39 21 PM

@deepakrkris deepakrkris self-assigned this Nov 19, 2019
@bajtos
Copy link
Member

bajtos commented Nov 19, 2019

Thank you for providing test instruction and a screenshot of the outcome. Just FYI, the instructions could be simplified by taking one of our example applications (e.g. examples/todo) and changing the param decorators for the filter query in that app.

@bajtos
Copy link
Member

bajtos commented Nov 19, 2019

It's great to see that swagger-ui can handle json-encoded query parameters. Now we need to carefully consider backwards compatibility, see #3770 (comment).

An important aspect to consider: if we switch the spec of format argument from deepObject-style to json-content, how is it going to affect the actual REST API? Will it mean that clients will be no longer able to send queries like find?filter[limit]=1 and will have to always use JSON encoding only (find?filter={"limit":1})? Can we find a way how to preserve support for both flavors?

@@ -189,7 +189,7 @@ describe('TodoApplication', () => {

await client
.get('/todos')
.query({filter: {where: {isComplete: false}}})
.query({filter: encodeURIComponent(JSON.stringify({where: {isComplete: false}})) })
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, both flavors should be supported:

  • filter: {where: {isComplete: false}}
  • filter: encodeURIComponent(JSON.stringify(...))

Instead of changing this existing test, we should add a new test to verify the other flavor. I believe these two flavors are already supported now, in which case it would be great to have tests to verify all different ways how to provide the filter argument.

Copy link
Member

Choose a reason for hiding this comment

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

(This code change & discussion thread is directly related to my previous comment about backwards compatibility.)

Copy link
Member

Choose a reason for hiding this comment

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

An idea to consider & try: can we mix deepObject style with json style in a single parameter? Something along the following lines:

    object: function(
      name: string,
      schema: SchemaObject | ReferenceObject = {
        type: 'object',
        additionalProperties: true,
      },
      spec?: Partial<ParameterObject>,
    ) {
      schema = {
        type: 'object',
        ...schema,
      };
      return param({
        name,
        in: 'query',

        // deepObject
        style: 'deepObject',
        explode: true,

        // new addition - json encoding
        content: {
           "application/json": {
             schema
           }
         },

         // the rest
        schema,
         ...spec,
       });
     },

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 Let's make sure the default signature filter: {where: {isComplete: false}} still works.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally, both flavors should be supported:

  • filter: {where: {isComplete: false}}
  • filter: encodeURIComponent(JSON.stringify(...))

@bajtos while using super-test both the above are the same , ie, using encodeURIComponent(JSON.stringify( for super test is redundant, while for other http client invocations it is required. sending just filter: {where: {isComplete: false}} in url will not work.
I suppose you are asking to support exploded query params like filter[limit]=1

Instead of changing this existing test, we should add a new test to verify the other flavor. I believe these two flavors are already supported now, in which case it would be great to have tests to verify all different ways how to provide the filter argument.

yes both the exploded and complex objects are already supported in loopback, even without the changes in this PR. The suggested changes in this PR is only for API Explorer to support complex objects.

Copy link
Contributor Author

@deepakrkris deepakrkris Nov 20, 2019

Choose a reason for hiding this comment

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

@bajtos @jannyHou I have now implemented your suggestions to modify existing decorator with backward compatibility, and also support complex objects for query params in API explorer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bajtos @jannyHou , to be clear , currently loopback supports both exploded and uri encoded complex objects in query params, even without the changes suggested in this PR. The purpose of this PR is to ONLY fix API Explorer to support complex objects in query params.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bajtos @jannyHou , I have tested , for example apps API Explorer is working fine now for complex objects in query params.

@bajtos
Copy link
Member

bajtos commented Nov 19, 2019

Last but not least, please check why Travis CI is failing and try to make all tests pass.

I see the following compilation error, there is also a linting error.

packages/openapi-v3/src/decorators/parameter.decorator.ts(252,9): error TS1005: ',' expected.

Copy link
Contributor

@jannyHou jannyHou left a comment

Choose a reason for hiding this comment

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

@deepakrkris Great findings! commented. The solution is elegant and LGTM in general.

* @param name - Parameter name
* @param schema - Optional OpenAPI Schema describing the JSON object.
*/
jsonObject: function(
Copy link
Contributor

@jannyHou jannyHou Nov 19, 2019

Choose a reason for hiding this comment

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

The solution is cool!
Implementation details...I think we don't need a new decorator to support the multi contents, @bajtos 's comment is sth we could try https://github.com/strongloop/loopback-next/pull/4141/files#r347767523.

Or provide the content object as the 3rd parameter spec?: Partial<ParameterObject>

@deepakrkris You may want to check is it a valid spec to have schema and content both defined, like:

 // new addition - json encoding
content: {
    "application/json": {
         schema
     }
},

// the rest
// @jannyHou: Is it valid to have both `schema` and `content` defined?
 schema,

Copy link
Contributor

Choose a reason for hiding this comment

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

And I feel the new signature of param.query.object() would be very similar to @requestBody, which reminds me to have a chat with @bajtos on #3493, I will schedule a meeting asap and update our conclusion here. Anyway the spike is good enough to me :) We can determine the signature in the follow-up stories.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jannyHou I have modified existing decorator with backward compatibility and also fix API Explorer for complex objects in query params.

@@ -189,7 +189,7 @@ describe('TodoApplication', () => {

await client
.get('/todos')
.query({filter: {where: {isComplete: false}}})
.query({filter: encodeURIComponent(JSON.stringify({where: {isComplete: false}})) })
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 Let's make sure the default signature filter: {where: {isComplete: false}} still works.

@deepakrkris deepakrkris force-pushed the spikeUrlEncQueryParam branch 6 times, most recently from cd00442 to 303d3ff Compare November 20, 2019 04:46
@deepakrkris deepakrkris changed the title feat: spike on url encoding of json object in query param feat: spike on API Explorer supporting complex objects in query param Nov 20, 2019
Copy link
Contributor

@jannyHou jannyHou left a comment

Choose a reason for hiding this comment

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

Great, 👍

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

I love this solution too ❤️

Do we have any automated tests to verify that our applications are emitted valid OpenAPI spec documents? Can you please double-check?

From what I remember, there are two OpenAPI validators and sometimes one of them reports error for spec documents that are considered as valid by the other.

When I paste the spec for examples/todo-list to Swagger Editor (see https://editor.swagger.io), I get several "structural errors" reported, presumably one for each use of the new query param object style:

Structural error at paths./todos.get.parameters.0
should have either a `schema` or `content` property
Jump to line 868

The same error is reported by swagger validator (see https://apitools.dev/swagger-parser/online/):

Swagger schema validation failed. 
  Data does not match any schemas from 'oneOf' at #/paths//todos/get/parameters/0
    Data matches schema from 'not' at #/paths//todos/get/parameters/0
    Missing required property: $ref at #/paths//todos/get/parameters/0
 
JSON_OBJECT_VALIDATION_FAILED

See also https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/3.0.1.md#parameter-object, emphasis is mine:

The rules for serialization of the parameter are specified in one of two ways. For simpler scenarios, a schema and style can describe the structure and syntax of the parameter.
(...)
For more complex scenarios, the content property can define the media type and schema of the parameter. A parameter MUST contain either a schema property, or a content property, but not both. When example or examples are provided in conjunction with the schema object, the example MUST follow the prescribed serialization strategy for the parameter.

Let's find a way how to emit OpenAPI spec that's valid.

await givenTodoInstance(todoRepo, {title: 'todo2', todoListId: list.id});
await givenTodoInstance(todoRepo, {title: 'todo3', todoListId: list.id});

const response = await client.get('/todos').query('filter[limit]=2');
Copy link
Member

Choose a reason for hiding this comment

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

This is a bit redundant, because query({filter: {limit: 2}}) is automatically converted by supertest to ?filter[limit]=2. Having said that, I think it's a good idea to have tests that are explicitly verifying different encoding styles, as opposed to the current status, where such encodings are tested implicitly, see:

https://github.com/strongloop/loopback-next/blob/303d3ff6ebe751b238ab16e021f6a25f09f4a1be/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts#L110

https://github.com/strongloop/loopback-next/blob/303d3ff6ebe751b238ab16e021f6a25f09f4a1be/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts#L196-L198

Writing such tests is out of scope of this spike, but please do include a task/todo-item in the acceptance criteria of the story that will track the real implementation.

@bajtos
Copy link
Member

bajtos commented Nov 21, 2019

Considering that runtime supports both deepObject and json encodings (no changes are needed to fix API Explorer experience), and many of our users (hopefully?) don't care that much how object parameters are described in the spec, as long as it's a valid spec that's supported by API Explorer, I am leaning towards removing deepObject style from the generating spec and providing content only.

This can break existing applications (for example when using clients code-generated by a tool that may not support json encoding), therefore this change needs to be published as semver-major of @loopback/openapi-v3 and @loopback/rest.

Thoughts?

/cc @strongloop/loopback-next

@jannyHou
Copy link
Contributor

jannyHou commented Nov 25, 2019

@bajtos I am glad you verified manually that schema and content has conflict and found the related doc, good catch!

I am leaning towards removing deepObject style from the generating spec and providing content only.

Sounds good to me.

This can break existing applications (for example when using clients code-generated by a tool that may not support json encoding), therefore this change needs to be published as semver-major of @loopback/openapi-v3 and @loopback/rest.

Fair enough to have a semver-major release to support querying nested filter.

@deepakrkris
Copy link
Contributor Author

@bajtos @jannyHou , I have fixed the parameter spec. removed style , explode and schema fields. Now schema passes https://editor.swagger.io/ . But there are additional changes required in loopback to support extracting param schema from content field. Tested with API Explorer and browser, works fine.

@deepakrkris
Copy link
Contributor Author

@bajtos I have also removed the explode and style properties because https://editor.swagger.io/ complains for them as well (when content field is included in the schema)

@deepakrkris deepakrkris force-pushed the spikeUrlEncQueryParam branch 2 times, most recently from 3355247 to 5604f7b Compare November 27, 2019 17:43
@deepakrkris deepakrkris force-pushed the spikeUrlEncQueryParam branch from 5604f7b to 9218d22 Compare November 27, 2019 18:22
@deepakrkris
Copy link
Contributor Author

All the suggestions are implemented and tested and the CI passes, so I am closing this PR. findings of this spike are updated in the spike PR as well as the linked original issue.

if (!spec.schema && spec.content) {
const content = spec.content;
const jsonSpec = content['application/json'];
spec.schema = jsonSpec.schema;
Copy link
Member

Choose a reason for hiding this comment

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

This is ok in a spike, but please note that the real implementation must not mutate (modify) the spec object given to us by the caller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants