-
Notifications
You must be signed in to change notification settings - Fork 312
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
Add OptionalInput type to allow explicit null value detection #140
Conversation
} | ||
|
||
public void setNotes(OptionalInput<String> notes) { | ||
this.notes = (notes == null) ? OptionalInput.defined(null) : notes; |
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.
DataBinder.bind
doesn't pass null
values to the conversion service, so this setter can receive null
.
The condition here is more like a quickfix, and I hope someone knows a better way around this.
8cc8a46
to
cde6882
Compare
Hey @koenpunt , I'm still processing #139 with some refactoring and discussing with the broader team. I don't understand I'll review these changes after #139, but please note that for such improvements discussing things in an issue is a good first step if we want to avoid working on contributions that won't be integrated. |
See here for details on the concept of the The Java Although maybe a better, more Spring-like solution would be to introduce an additional annotation. |
31298ac
to
c973402
Compare
I think for an That said, like Brian, my concern is that such a type feels more broadly applicable. In a web application you can have a query parameter present with a value, present without a value, or not present at all. What did you have in mind with the additional annotation? Also I'm wondering is it really necessary to differentiate vs expecting a more explicit modeling of "null" input? |
I'm not sure I understand what you mean by this.
Didn't have anything in mind yet, but since a lot of things in Spring are handled with annotations it might be more fitting.
What do you mean by this? |
Oh I think you refer to the different nested classes for Undefined/Defined; that's mainly because that how this was implemented in graphql-kotlin, and Kotlin supports them in switch statements with pattern matching, allowing to use it like; val value = when (myParameter) {
is Defined -> myParameter.value
else -> null
} |
static class BookInput {
@Nullable
Optional<String> notes = null;
@Nullable
public Optional<String> getNotes() {
return this.notes;
}
public void setNotes(@Nullable Optional<String> notes) {
this.notes = notes;
}
} |
bd508e5
to
ae9dc5b
Compare
Yes.
I did not mean to suggest using This is just an aside but it does relate to a broader question. I'm hesitant to introduce a type that is positioned so closely as an alternative to Currently one can inject I would be more in favor of some wrapper type like |
That is far from convenient, especially when having more complex/nested input types. I do think it's quite common, but maybe that's only from my perspective. A rudimentary example; having a mutation to update 2 properties of a resource, for example type Mutation {
updateResource(input: UpdateResourceInput!): UpdateResourcePayload!
}
input UpdateResourceInput {
name: String
description: String
} In the meantime, would a PR be accepted to have the possibility to "intercept" the conversion/instantiation proces, so custom types like this can be implemented in user space? |
1fc847b
to
7a52b0c
Compare
For more flexibility with argument resolving, maybe a specific constructor or method could indicate to the argument resolver that it should be used to instantiate the object, instead of the built in data binder. Then it wouldn't be specific for optional inputs, but any arbitrary pre processing logic could be added for any argument. |
7a52b0c
to
bc8003e
Compare
@koenpunt apologies for the delay. We had a team discussion last week and I've been meaning to comment. The required flag on We're now wondering about approaches. Perhaps a programmatic API to navigate the Map for an input argument with conversion/binding support could be useful. I see your proposal under #151 along similar lines, although I wonder how or if that's intended to work on more deeply nested fields? Other ideas could be JSONPath to check for presence/absence, or projection interfaces that mirror the input types to navigate to sub-parts and check for presence/absence. Before we go farther, I would like to ask for some clarification. In your solution, do you mean for |
It would be nice if we could differentiate between absence of fields and explicit nulls. One use-case for this is supporting partial update mutations with nullable values. In those cases you need a tri-state input field like this example:
This way the client can use a single mutation to update multiple fields of an entity without the need to specify all values in the input type or accidentally setting values to |
Thanks for commenting @jurriaan. Yes the case for partial update is clear. As per my questions to @koenpunt in #140 (comment), I would also be interested in your feedback. How do you work with and persist such input? Obviously without a dedicated type, it's impossible to differentiate null from undefined, but a dedicated type would also be a challenge for JPA and others. Or do you manually check a few top-level fields and then treat their values as aggregates without making such checks at lower levels? The general question is how to expose this Map of data in a way that is convenient for the way it will actually be used. |
We do have a layer separating the GraphQL and JPA layers where we do some sanitizing / extra checks anyway, and these checks are done on multiple layers of the input objects (i.e. we have nested input objects containing fields of If this is too far fetched to implement in this codebase it would help if there is some kind of interface where we could add our own mappers to map the GraphQL input to some kind of Optional input type ourselves. |
FYI @jurriaan and I are working on the same team.
To answer your questions;
I indeed do think it should be possible to use this at any level.
To give an example of our implementation; We have a mutation with an input type; type Mutation {
productUpdate(productId: ID!, input: ProductInput!): ProductPayload!
}
input ProductInput {
"The description of the product."
description: String
"The name of the product."
name: String
"The price of the product."
price: Decimal
} The implementation looks a bit like this; fun productUpdate(productId: ID, input: ProductInput): ProductPayload {
val name: Option<String> = when (input.name) {
is OptionalInput.Defined -> {
val value = input.name.value ?: throw IllegalArgumentException("Name can not be set to null")
Some(value)
}
is OptionalInput.Undefined -> none()
}
// ...
} Where you can see we validate, and transform the input type to an Later this optional is passed to an update method in our service class, which looks something in the lines of; fun updateEntity(product: Product, name: Option<String>, description: Option<String?>, price: Option<BigDecimal?>): Product {
name.map { product.name = it }
description.map { product.description = it }
// ... Where So no, we don't have a direct mapping with our JPA entities, we have custom logic in place for that. As you see we're not necessarily looking for an So to echo @jurriaan's suggestion; having an interface to provide custom input mappers would probably be the most flexible. NB. Data validation as is shown in the example above should probably be moved to a schema annotation, having the input be rejected at query parse time by using a custom directive. |
Thanks for the extra details. I think we could allow the |
I noticed that the conversion service doesn't receive null values (see the implementation of the |
A quick update: The conversion service added in 6975e6c covers a lot of the use cases I wanted to fix with this PR. By defining various static methods like However properties with nested non-simple types can't, because those are passed as a map. I could add an additional This wouldn't be necessary once #155 gets merged, but then there's the problem that it tries to instantiate the I didn't yet figure out how to fix this, but maybe support for customization of the instantiator would allow wrapping and unwrapping custom types. |
Some time has passed, and we still have hacks in place to support an "optional input" type, and thus I would still like to see a proper solution for this. I did notice that it's now possible to assign a conversion service to the databinder, using |
In graphql there's a difference between a null passed as input, and not providing the input value at all.
Inspired by the type that graphql-kotlin provided, I've introduced an
OptionalInput
wrapper, which can be used to distinguish between explicit null values and absent values.Builds upon #139