-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Spike/new requestbody #3466
Closed
Closed
Spike/new requestbody #3466
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
## Improve the UX of @requestBody() | ||
|
||
The original discussion is tracked in issue | ||
[Spike: simplify requestBody annotation with schema options](https://github.com/strongloop/loopback-next/issues/2654). | ||
|
||
The current @requestBody() can only | ||
|
||
- takes in an entire request body specification with very nested media type | ||
objects OR | ||
- generates the schema inferred from the parameter type | ||
|
||
It doesn't give people the convenience to configure the schema spec with `model` | ||
and `schemaOptions` without defining them in a nested object. To simplify the | ||
signature, this spike PR introduces two more parameters `model` and | ||
`schemaOptions` to configure the schema. | ||
|
||
Please note the 2nd and 3rd parameters are only used to contribute the spec for | ||
_default schema_(I will explain what default means with an example), any other | ||
request body spec are still defined in the 1st parameter. | ||
|
||
See the example from the swagger official website: | ||
https://swagger.io/docs/specification/describing-request-body/ | ||
|
||
- `application/json` and `application/xml` share the same schema | ||
`#/components/schemas/Pet` | ||
- `application/x-www-form-urlencoded` uses another schema | ||
`#/components/schemas/PetForm' | ||
- `text/plain` receives a string: `type: string` | ||
|
||
The new decorator allows | ||
|
||
- a default schema (generated from the 2nd and 3rd params, should be `Pet` in | ||
this case) _and_ | ||
- custom schemas (provided by the 1st parameter: partial spec, `PetForm` and | ||
string schema) | ||
|
||
so that developers don't need to repeat defining the same schema in the 1st | ||
parameter. | ||
|
||
The logic of the new decorator is: | ||
|
||
- The 1st parameter contains the pure OpenAPI request body spec | ||
- Other parameters contribute the default schema' config, `model`, `options`. | ||
- The decorator: | ||
- first reads schema config | ||
- then generate default schema spec accordingly | ||
- if any content type is missing `schema`, then assign the default schema to | ||
it(Our current implementation is already doing it, see | ||
[code](https://github.com/strongloop/loopback-next/blob/master/packages/openapi-v3/src/decorators/request-body.decorator.ts#L103-L107)) | ||
|
||
ANY OTHER PARTS of the spec are read from the 1st parameter, the decorator only | ||
generate and merge the default `schema`. We can implement helpers/builders for | ||
developers to build the entire spec, like | ||
|
||
```ts | ||
requestBodyBuilder | ||
.addContent('application/x-www-form-urlencoded') | ||
.withSchema(model, options) | ||
.withExample(exampleSpecs); | ||
``` | ||
|
||
The new decorator `requestBody2()`(let's discuss a better name later, see | ||
section [Naming](#Naming) is written in file 'request-body.spike.decorator.ts' | ||
|
||
### Signature | ||
|
||
The new decorator's signature is | ||
`@requestBody2(spec, modelCtor, schemaOptions)`. | ||
|
||
All the 3 parameters are optional, therefore in the PoC, the real implementation | ||
are in `_requestBody2()`, `requestBody2()` is a wrapper that resolves different | ||
combination of the parameters. | ||
|
||
My PoC PR only adds 2 unit tests for different signatures, the real | ||
implementation should test all the combinations. | ||
|
||
```ts | ||
export function _requestBody2<T>( | ||
specOrModelOrOptions?: Partial<RequestBodyObject>, | ||
modelOrOptions?: Function & {prototype: T}, | ||
schemaOptions?: SchemaOptions, | ||
) { | ||
// implementation | ||
} | ||
``` | ||
|
||
### Create - exclude properties | ||
|
||
Take "Create a new product with excluded properties" as an example: | ||
|
||
```ts | ||
// The decorator takes in the option without having a nested content object | ||
class MyController1 { | ||
@post('/Product') | ||
create( | ||
@requestBody2( | ||
// Provide the description as before | ||
{ | ||
description: 'Create a product', | ||
required: true, | ||
}, | ||
// Using advanced ts types like `Exclude<>`, `Partial<>` results in | ||
// `MetadataInspector.getDesignTypeForMethod(target, member)` only | ||
// returns `Object` as the param type, which loses the model type's info. | ||
// Therefore user must provide the model type in options. | ||
Product, | ||
// Provide the options that configure your schema | ||
{ | ||
// The excluded properties | ||
exclude: ['id'], | ||
}, | ||
) | ||
product: Exclude<Product, ['id']>, | ||
) {} | ||
} | ||
``` | ||
|
||
### Update - partial properties | ||
|
||
```ts | ||
class MyController2 { | ||
@put('/Product') | ||
update( | ||
@requestBody2({description: 'Update a product', required: true}, Product, { | ||
partial: true, | ||
}) | ||
product: Partial<Product>, | ||
) {} | ||
} | ||
``` | ||
|
||
## Naming | ||
|
||
From @jannyHou: I think we can keep the name `requestBody` unchanged. I enabled | ||
the existing `@requestBody()`'s tests but applied `requestBody2()` decorator, | ||
all tests pass, which means there is no breaking change. | ||
|
||
## Follow-up Stories | ||
|
||
- [ ] More discussion | ||
|
||
- The signature: | ||
- `@requstBody(spec, model, options)` | ||
- or `@requestBody(specWithOptions, model)` | ||
- or `@requestBody(spec, getModelSchemaRef(model, options))` | ||
- or any better signatures | ||
- Which signatures should be disabled | ||
|
||
- [ ] Implementation | ||
- Modify the current `@requestBody()` according to the spike code, pass | ||
existing tests across all repos. | ||
- Add more unit tests to verify all the signatures. | ||
- Fix the type conflict detect, see test case 'catches type conflict', and | ||
discussion in | ||
[comment](https://github.com/strongloop/loopback-next/pull/3466/files#r308657211) | ||
- [ ] Upgrade examples to provide the descriptive configs(model, options) using | ||
the new decorator. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 like to tighten the API little bit, to make it more difficult for our users to do something wrong or confusing.
(1)
When the spec contains
content
property, then we should not allowmodel
&schemaOptions
.I'd like to make the following example invalid & rejected by the compiler.
Proposed signature (overload):
(2)
As discussed in #3398 (comment), we may want to add a new overload in the future that will allow users to a different ORM framework supply the
MediaTypeObject
inrequestBody
parameters, for example:I'd like our current proposal to be forward compatible and allow us to add support for the example above as an incremental & backwards-compatible change in the future.
IIUC the current proposal, it supports the following form:
I am concerned that if we want to support
MediaTypeObject
for the second argument too, then it will be difficult to determine what did the user mean - did they provide JsonSchemaOptions or MediaTypeObject?To avoid that problem, I am proposing to forbid the following form:
and always require users to provide the model constructor if they want to use JsonSchemaOptions too.
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.
My point (2) becomes irrelevant if we decide to nest
schemaOptions
as a new property of the first argument, see #3466 (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.
@bajtos I don't think excluding the
content
for the first parameterspec
is a good idea, the 2nd and 3rd parameters are there to describe the schema, butcontent
has more stuff thanschema
, and user should still have their own flexibility tocontent
includingschema
without using the 2nd and 3rd paramsapplication/x-www-form-urlencoded
in this exampleSo honestly, IMO the request spec(especially content) is nested in nature, unless we provide a bunch of fluent APIs to help users build it, like
requestBodyBuilder.addContent('application/x-www-form-urlencoded').withSchema(model, options).withExample(exampleSpecs)
, there is not that much to simplify for the UX...and if we implement those builders, the purpose is still generating a complete requestBody spec, which can be provided as the 1st parameter in@requestBody()
, and still fits my current proposal.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.
My point of view:
content
object in the first argument.content
object for the given model & schema options.If you like to go forward with your proposal, then please document the rules we are going to merge the (deep) fields in
spec.content
with the fields created from model + schemaOptions.I am also interested how are developers going to learn about those merge rules and more importantly, when they do a mistake, how are they going to figure out why they are getting different schema from what they were expected to see?
Let's start with the example I have shown before:
Another case to consider:
Are we going to print debug logs or perhaps regular warnings to notify developers when the framework is overwriting spec in a way that may be unexpected?
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.
@bajtos
Let's still discuss with the example from the swagger official website:
https://swagger.io/docs/specification/describing-request-body/
application/json
andapplication/xml
share the same schema#/components/schemas/Pet
application/x-www-form-urlencoded
uses another schema `#/components/schemas/PetForm'text/plain
receives a string:type: string
My point is we could allow
Pet
in this case)and
PetForm
and string schema)so that developers don't need to repeat defining the same schema in the 1st parameter.
And our current implementation is already doing it, see code:
schema
is missing,content.schema = defaultSchema
.Back to your use case:
This is a VALID use case IMO.
cc @strongloop/sq-lb-apex @raymondfeng more opinions are welcomed here ^^ :)
With your proposal which merges the
JsonSchemaOptions
into the 1st parameter, everything would be the same...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.
@bajtos hope my comment #3466 (comment) answers your question in #3466 (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.
@bajtos I would suggest NOT achieve this by adding more signatures by overloading, we can introduce fluent spec builder functions to generate any part of the requestBody spec, they would be much more straightforward to use than providing partial specs in some parameters and the decorator function resolves different parts from different places.
The logic in the current decorator, or my new proposal is pretty straightforward to understand IMO:
model
,options
...schema
, then assign the default schema to itANY OTHER PARTS of the spec are read from the 1st parameter, the decorator only generate and merge the default
schema
. We can implement helpers/builders for developers to build the entire spec, likeThere 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.
After discussing with Janny, I think her proposal makes sense to me high-level(ly). Especially the idea of that we can have some build functions that similar to this that allows users to add/build spec object if they want to pass in more information.
But I am not sure if @jannyHou 's proposal could address the following requirement that @bajtos mentioned above:
If yes, I am fine with the proposal.
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 agree with @agnes512, but for the first iteration, let's not exclude the content for the first parameter. I am not knowledgable to know if the use-case pointed by @bajtos is either valid or not valid since we have differing opinions, so I think we should explore it further outside the scope of this PR.