-
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
Spike/new requestbody #3466
Conversation
|
||
const paramType = paramTypes[index]; | ||
|
||
// preserve backward compatibility |
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.
FYI: compared with the current decorator function, the difference of the new decorator is from Line 81 to Line 91, other code stay the same.
fa2b2c7
to
f1b596f
Compare
import { belongsTo, Entity, hasMany, model, property } from '@loopback/repository'; | ||
import { expect } from '@loopback/testlab'; | ||
import { getControllerSpec, post, put } from '../../../..'; | ||
import { requestBody2 } from '../../../../decorators/request-body.spike.decorator'; |
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.
Formatting is broken.
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.
@raymondfeng Fixed :)
05c63dc
to
9c142bb
Compare
9c142bb
to
0a8df75
Compare
_Please note TypeScript doesn't support function overloading with different | ||
number of parameters(like requestBody(spec), requestBody(model, options)). | ||
Therefore we have to create a wrapper function to resolve different signatures | ||
from the caller_ |
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 afraid this is not true. The following code seems to compile correctly for me.
export function requestBody2(
spec: Partial<RequestBodyObject>,
): (target: object, member: string, index: number) => void;
export function requestBody2(
spec: Partial<RequestBodyObject>,
model: Function,
schemaOptions?: SchemaOptions,
): (target: object, member: string, index: number) => void;
export function requestBody2(
spec: Partial<RequestBodyObject>,
schemaOptions?: SchemaOptions,
): (target: object, member: string, index: number) => void;
export function requestBody2(
schemaOptions?: SchemaOptions,
): (target: object, member: string, index: number) => void;
export function requestBody2(
specOrModelOrOptions?: Partial<RequestBodyObject> | Function | SchemaOptions,
modelOrOptions?: Function | SchemaOptions,
schemaOptions?: SchemaOptions,
): (target: object, member: string, index: number) => void {
return (target: object, member: string, index: number): void => {
// implementation
};
}
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 This is weird, not sure if you read the comment in #3398 (comment)
yesterday I double checked it with Dom and I believe the conclusion is overloading with different number of parameters is not supported, and my editor still complains with your 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.
What version of TypeScript are you using in the editor? I vaguely remember that VS Code sometimes prefers to use the built-in version instead of the version installed in node_modules
. Could that explain the difference?
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.
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 have the same problem with 3.5.3, see:
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 overloaded functions work for me in VSCode and npm run build
:
export function requestBody2(
spec: Partial<RequestBodyObject>,
): (target: object, member: string, index: number) => void;
export function requestBody2<T extends object>(
spec: Partial<RequestBodyObject>,
model: Function,
schemaOptions?: JsonSchemaOptions<T>,
): (target: object, member: string, index: number) => void;
export function requestBody2<T extends object>(
spec: Partial<RequestBodyObject>,
schemaOptions?: JsonSchemaOptions<T>,
): (target: object, member: string, index: number) => void;
export function requestBody2<T extends object>(
schemaOptions?: JsonSchemaOptions<T>,
): (target: object, member: string, index: number) => void;
export function requestBody2<T extends object>(
specOrModelOrOptions?:
| Partial<RequestBodyObject>
| Function
| JsonSchemaOptions<T>,
modelOrOptions?: Function | JsonSchemaOptions<T>,
schemaOptions?: JsonSchemaOptions<T>,
) {
//
}
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.
Seems like it works for 2/3 of us, so this would be a great addition/change to us having to determine the arguments from the callee.
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.
k...finally get rid of the error after several times' code comment/uncomment 👏 , but still have no clue why that error happened...
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 proposal looks much better now!
...s/openapi-v3/src/__tests__/unit/decorators/request-body/request-body.spike.decorator.unit.ts
Show resolved
Hide resolved
schemaOptions?: SchemaOptions, | ||
) { | ||
// implementation | ||
} |
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 update the signature to match the code.
export function requestBody2<T extends object>(
specOrModelOrOptions?:
| Partial<RequestBodyObject>
| Function
| JsonSchemaOptions<T>,
modelOrOptions?: Function | JsonSchemaOptions<T>,
schemaOptions?: JsonSchemaOptions<T>,
) {
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 reason why I put the private function _requestBody2()
's signature(instead of the wrapper's) here is to avoid the confusion caused by manually resolve the caller's input.
Let me change the name to the private one.
|
||
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. |
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 allow model
& schemaOptions
.
I'd like to make the following example invalid & rejected by the compiler.
requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(MyModel)
}
},
MyModelData, // notice a different class!
);
Proposed signature (overload):
export function requestBody2<T>(
spec: Omit<Partial<RequestBodyObject>, 'content'>,
model: Function,
schemaOptions?: JsonSchemaOptions<T>,
): (target: object, member: string, index: number) => void;
(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
in requestBody
parameters, for example:
@requestBody(
// fields from RequestBodyObject
{
description: 'description of the request body parameter',
required: true,
},
// MediaTypeObject to use for populating `content` entries
{schema: getModelSchemaRef(Product, {exclude: ['id']})}
)
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:
@requestBody(
// RequestBodyObject
{description: 'description of the request body parameter'},
// JsonSchemaOptions
{partial: true}
)
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:
@requestBody(
// RequestBodyObject
{description: 'description of the request body parameter'},
// JsonSchemaOptions
{partial: true}
)
and always require users to provide the model constructor if they want to use JsonSchemaOptions too.
@requestBody(
// RequestBodyObject
{description: 'description of the request body parameter'},
MyModel,
// JsonSchemaOptions
{partial: 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.
(2)
I am proposing to forbid the following form
and always require users to provide the model constructor if they want to use JsonSchemaOptions
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, but content
has more stuff than schema
, and user should still have their own flexibility to
- provide an entire
content
includingschema
without using the 2nd and 3rd params - if the 2nd+3rd params decide the default schema, user can override it for special media types with different schemas, like
application/x-www-form-urlencoded
in this example
So 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:
- If users want to override special media types, then they provide full
content
object in the first argument. - The 2nd+3rd params are only for the case when they want LB to generate default
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:
requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(MyModel)
}
},
MyModelData, // notice a different class!
);
Another case to consider:
requestBody({
content: {
'application/json': {
example: {/* some model data */
}
},
MyModel,
);
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.
- When the spec contains content property, then we should not allow model & schemaOptions.
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
- 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.
And our current implementation is already doing it, see code:
- we generate a default schema according to the interred type
- iterate through the contents, if
schema
is missing,content.schema = defaultSchema
.
Back to your use case:
requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(MyModel)
}
},
MyModelData, // notice a different class!
);
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.
- As discussed in [SPIKE] feat: simplify request body decorator #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 in requestBody parameters...
@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:
- The 1st parameter contains the pure OpenAPI request body spec
- Other parameters contribute the default schema' config,
model
,options
... - The decorator:
- reads schema config
- generate default schema spec accordingly
- if any content type is missing
schema
, then assign the default schema to it
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
requestBodyBuilder.addContent('application/x-www-form-urlencoded').withSchema(model, options).withExample(exampleSpecs)
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.
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:
(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 theMediaTypeObject
inrequestBody
parameters..
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.
| Partial<RequestBodyObject> | ||
| Function | ||
| JsonSchemaOptions<T>, | ||
modelOrOptions?: Function | JsonSchemaOptions<T>, |
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 type safe, we allow users to provide MyModel
as the model ctor but then provide JsonSchemaOptions<OtherModel>
.
I think we should use Function & {prototype: T}
to require the callers to provide a constructor function of T
instead of arbitrary function.
I checked the source code, this problem is present in the current implementation of modelToJsonSchema
, getJsonSchema
, getJsonSchemaRef
and getModelSchemaRef
functions too. Let's fix that in a standalone pull request, before we start any work on a better @requestBody
decorator - see #3470
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 able to catch the non-existing fields in exclude
by using Function & {prototype: T}
, but
@requestBody<RandomModel>(spec, Product) test: Product
still passes.
I added a test case for it, let's fix it in the implementation.
66ffa8f
to
655a173
Compare
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 proposal at a high level LGTM, especially using the default schema. I agree with @bajtos' point about very clearly documenting the merge rules so that developers are aware of how it works. As for using a different class in e.g.:
requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(MyModel)
}
},
MyModelData, // notice a different class!
);
At first glance, I thought it should be rejected, but from your POV why do you think it should be a valid use 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.
Nice job Janny. LGTM overall, left my views on some of the discussions.
}, | ||
}); | ||
|
||
// The feature that generates schemas according to |
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 include this in the list of follow up stories in the markdown file you have (disregard if already included)?
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.
@b-admike Seems this feature is already supported.
|
||
const spec2 = getControllerSpec(MyController2); | ||
|
||
const requestBodySpecForCreate = |
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.
nitpick: requestBodySpecForUpdate
_Please note TypeScript doesn't support function overloading with different | ||
number of parameters(like requestBody(spec), requestBody(model, options)). | ||
Therefore we have to create a wrapper function to resolve different signatures | ||
from the caller_ |
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.
Seems like it works for 2/3 of us, so this would be a great addition/change to us having to determine the arguments from the callee.
|
||
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. |
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.
655a173
to
5877f0c
Compare
See my explanation in https://github.com/strongloop/loopback-next/pull/3466/files#diff-12798cc8f728c5affaa88ab994dbf31fR21, and also comment. IMO user should have the flexibility to define a default schema using the 2nd and 3rd parameters, but override it with custom ones provided in the 1st parameter. |
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 the explanations 👍
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
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
Subsequent tasks see #2654 (comment) |
Description
connect to #2654
A new draft PR created after all the discussions in #3398.
The PoC in #3398 is over complicated, and the rebase effort is also big after introducing the generic type in interface
JsonSchemaOptions
, so I created this PR to replace it.Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated👉 Check out how to submit a PR 👈