-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Omittable input fields #2585
Omittable input fields #2585
Conversation
Cool! I would love to get some other people to look it over and take it for a spin. |
For anyone interested, here's a barebones repository I put together to play around with this: https://github.com/Desuuuu/gqlgen-optional-test |
This is great! Would love to see some tests using |
You're calling the Optional's Stringer, which currently returns an empty string if the value is undefined, right there: https://github.com/carldunham/gqlgen-optional-test/blob/bf0d7f4e468044de25fc8f21002ac707ebafbf3d/graph/model/extensions.go#L6 I included it mostly for convenience/debugging but maybe printing something like JSON marshalling (with the standard library at least) wouldn't be great since type Optional[T any] struct {
value T
}
func OptionalOf[T any](value T) *Optional[T] {
return &Optional[T]{
value: value,
}
}
func (o *Optional[T]) IsDefined() bool {
return o != nil
}
func (o *Optional[T]) IsUndefined() bool {
return o == nil
}
func (o *Optional[T]) Value() (T, bool) {
if o == nil {
var zero T
return zero, false
}
return o.value, true
}
func (o *Optional[T]) ValueOr(def T) T {
if o == nil {
return def
}
return o.value
}
func (o *Optional[T]) String() string {
if o == nil {
return "<undefined>"
}
return fmt.Sprintf("%v", o.value)
}
func (o Optional[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(o.value)
}
func (o *Optional[T]) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &o.value)
} In this case, every field would have to be an |
Ah, got it. Would still like to see those tests :) |
I added tests with different types and changed the Stringer's output for undefined values. Any thoughts regarding |
I generally prefer |
For small structs like this, meant to be embedded, it can be nice to avoid the indirection. But I would prefer nil meaning “not set” over having a bool field for that.
|
There are two reasons to use a pointer receiver:
In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both. Am I correct in understanding:
|
It's not really the value that's nil in the I don't think it there's much of a difference for the copy, because As for mutability, I don't think it really adds anything for the user, so I'd prefer having methods to retrieve the value (something like The downsides of
While the upside is that marshaling the input to JSON with the standard library is more correct (undefined values are not marshaled instead of being null). I have a hard time seeing the use-case for marshaling input structs to JSON though. For these reasons |
@Desuuuu It seems like a perfectly reasonable approach to me, it requires dealing with different types down the line so more work adapting existing code, but overall cleaner than the other approaches before we had generics, if you want minor feedback:
Edit: Also, definitely support |
I thought it might be useful for some people, but in the context of this PR it does not serve any purpose indeed. Same goes for
It's kinda similar to the standard library printing nil pointers as
The name is not in any way linked to GraphQL, it's simply a Go type that fields can be bound to. It will not appear anywhere in the schema. Maybe we can find a better name for
No, it's redundant. But you can do I'm thinking the type could also be: type Optional[T any] struct {
value *T
defined bool
} Which would mean the value can always be nil. |
Yeah, that's what I mean, it can get conflated with something else, that is, people don't read docs in depth and might confuse this as a way to have "optional" input fields, maybe
might get a bit messy with mixed pointer and non-pointer T no? imo your current type implementation is less opinionated about the underlying type it handles which is good |
It's definitely more opinionated, since it forces you to handle nil values. I'm not sure but I think modelgen always generates pointer types for nullable input fields, so it would not affect that but it would indeed prevent you from treating null values as zero values with custom input models. |
@Desuuuu It does always generate pointer types, but that assumes people only use modelgen models, in a lot of cases (mine included) this is not how it is used, types are created somewhere else and often represent null following |
I'm not too familiar with SQL in Go, but if the intent is to use the type directly, is there a way to make the driver work correctly with If that was possible and a use-case we wanted to support, there would be pretty much no reason to use Overall I think trying to make We could either add that later or even allow the user to use his own generic types somehow. Edit: Besides that, you can already differentiate between null and undefined for custom types that have an unmarshaler! |
This is not really about SQL, NullType is a common way to represent null instead of *Type, I don't use it for SQL in my case but rather interacting with another legacy system that uses this approach. The purpose is not to pass onward Optional[NullString] but rather to be able to extract all types the same way, meaning both Optional[*string] and Optional[NullString] can be converted respectively into *string and NullString without caring for how the underlying mechanism works
Imo there are just too many assumptions there about the behavior and limitations down the line, I'd much rather fit gqlgen to the rest of the system, not vice versa. Up to you if you consider it a use-case you want to support.
How can that be done without using this Optional implementation? an umarshaler would simply not be called if it is not defined, meaning we'd need to create a custom optional wrapper for every type separately eg:
|
Sorry, I misunderstood your comment. I thought it was about making the type interact with third-party packages.
That's a really good point against forcing pointers which I wasn't thinking about! I also agree with you that |
Right, so the nil *Optional can mean not provided, and a nil *T would mean a null value. I'm not arguing in favor, necessarily, but it seems to clearly reflect the semantics of a "nullable" input field (or argument?) that can be provided or not, and where that is meaningful. FWIW, I prefer avoiding terms like "optional" and "undefined", as they don't clearly reflect GraphQL semantics. Those are terms most often used to map gql concepts into language-specific ones, as I've seen them. Which is why I'm a bit hesitant about this discussion at all, but I'm aware that in the real world there are implementations that distinguish between "set" and "null" for things like partial object updates. I've provided PRs for that use case myself. 🙂 Either option chosen here would be cleaner than those. |
this solves something i've been scratching my head at so would love to see it merged 😅 |
I guess it is not possible to just leverage optional vs mandatory from graphql, with |
I think exposing that kind of API to the user would be confusing/prone to mistakes.
Would you prefer IMO we shouldn't be trying to map to GraphQL's nullability concept since this is specific to input fields and nullability itself is already handled farily well (and not necessarily with pointers as discussed earlier).
For generated models, there's an option to wrap all nullable input fields by default. I think that's the best we can do without breaking compatibility for existing users. |
Unless you intend to reinvent all the types you can't really do that, that would just get you in the same end result of having an Optiona[T] like implementation but with more extra steps. The issue is representing 3 distinct states, eg:
This is simply not possible to express with |
Where would defaults fall here? I assume the executor is dropping them in if values aren't provided, but also smart enough to replace with |
I updated the name to I also made its fields unexported, added a few methods to read its content and removed the extra methods which weren't really serving any purpose.
Just remembered that input fields can have default values. The field's value will always be defined in this case and the value will either be the default value or the actual value. I'm not sure what would be expected in this case. |
Makes sense. I've run into issues in the past over nullable arguments or input fields with defaults with a |
@carldunham I would think that is the intended behavior there, no? I guess supporting optional eg:
|
Well this is by the GraphQL spec, language independent. CarlOn Apr 1, 2023, at 09:10, Itamar Maltz ***@***.***> wrote:
@carldunham I would think that is the intended behavior there, no? I guess supporting optional database/sql style scanner interface makes sense if somebody wants some unique behavior for defaults, there is already a standard library example on how to tackle a similar scenario
eg:
func (v *MyNullableDefaultType) Scan(value any) error {
if any == nil {
// do some v stuff representing a different default
}
}
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: ***@***.***>
|
I updated the PR to reflect the changes that have been made, squashed and rebased. |
This PR adds the
Omittable
type to thegraphql
package. Its purpose is distinguishing between omitted and null input fields and it has the following API:The type is meant to be bound with nullable input fields.
Model generation is able to generate omittable fields by:
@goField(omittable: true)
directive on a nullable input field.nullable_input_omittable
configuration option totrue
. This causes nullable input field to be omittable by default.The
goField
directive always has priority over thenullable_input_omittable
option meaning you can have nullable input fields default to being omittable, and override it with@goField(omittable: false)
.Relevant issue: #1416
I have: