-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Only check used input arguments visibility #2985
Only check used input arguments visibility #2985
Conversation
Is this going to break |
@eapache Where are we using |
lib/graphql/schema/input_object.rb
Outdated
argument_result = argument.type.validate_input(input[name], ctx) | ||
if !argument_result.valid? | ||
result.merge_result!(name, argument_result) | ||
input.merge(required_inputs).each do |argument_name, value| |
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.
if we merged this the other way around we could build required_inputs
much more simply?
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 think the solution here using select looks something like:
required_inputs = self.arguments.select do |argument_name, argument|
!input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
end.map { |argument_name, argument| [argument_name, nil] }.to_h
I don't think this is too much better than the reduce, but can change if necessary.
KCCO I made some bad assumptions based on how you worded the PR description. |
1bae945
to
b14b8c3
Compare
result.add_problem("Field is not defined on #{self.graphql_name}", [name]) | ||
# Inject missing required arguments | ||
required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)| | ||
if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name) |
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.
Will get_argument
still trigger our logging? 'cause we're still calling it here on keys that are explicitly not in the input.
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.
Yes it will still trigger a call to visibility.
There isn't a great way around this with required arguments however, as we need to check if they are visible on the field in order to ensure that a value is being passed in for those arguments. If the argument is not visible then we don't care about it being required.
The only case we will trigger a visibility check and over-report our logging is when there is a required argument that is not supplied by the user and it is visible on the field.
If it is invisible we will still do the visibility check but it will not log.
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 only case we will trigger a visibility check and over-report our logging is when there is a required argument that is not supplied by the user and it is visible on the field.
These requests are broken anyway, correct, meaning they'll fail with an error for a missing argument?
I think this is probably a solid upstream improvement and good short-term fix for our issue (since IIRC none of the args we care about short-term are required), but the corner case worries me a bit. Let's ship this, but continue to think about a long-term solution.
result.add_problem("Field is not defined on #{self.graphql_name}", [name]) | ||
# Inject missing required arguments | ||
required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)| | ||
if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name) |
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 only case we will trigger a visibility check and over-report our logging is when there is a required argument that is not supplied by the user and it is visible on the field.
These requests are broken anyway, correct, meaning they'll fail with an error for a missing argument?
I think this is probably a solid upstream improvement and good short-term fix for our issue (since IIRC none of the args we care about short-term are required), but the corner case worries me a bit. Let's ship this, but continue to think about a long-term solution.
lib/graphql/schema/input_object.rb
Outdated
if visible_arguments_map[name].nil? | ||
result.add_problem("Field is not defined on #{self.graphql_name}", [name]) | ||
# Inject missing required arguments | ||
required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)| |
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 might call this missing_required_inputs
@@ -106,6 +106,12 @@ def get_field(parent_type, field_name) | |||
@visible_parent_fields[parent_type][field_name] | |||
end | |||
|
|||
# @return [GraphQL::Argument, nil] The argument named `argument_name` on `parent_type`, if it exists and is visible | |||
def get_argument(parent_type, argument_name) |
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 believe the intent of this method was to be cached which it is not right now?
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.
visible_argument?(argument)
is cached through the visible?
method on types. So I believe this is ok to leave non-cached?
I can make it more similar to the above methods though and cache it further.
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'll defer to @rmosolgo's judgement here. I did note that we are now calling get_argument
twice on the same argument in some scenarios.
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.
Yeah, the call to the application's implementation would be cached, I don't know whether using read_through { ... }
here is worth it or not, no preference without actually seeing a benchmark.
end | ||
# Items in the input that are expected, but have invalid values | ||
argument_result = argument.type.validate_input(value, ctx) |
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.
@rmosolgo it's weird to me that we handle missing required inputs by adding them as nil, and then letting the validation fail that nil isn't an accepted value? Could we not just add_problem
directly when we notice they're missing?
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.
Yah I was also not sure about this behaviour, I would assume this should actually be handled by a different part of code.
We can add a specific problem if we notice they are missing rather than doing what I am currently doing.
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.
adding them as nil
Do you mean, it worked this way in the old code? AFAIK the old code checked each argument and made sure that the provided value was valid, without special nil handling. I see that in the proposed change here, it has to handle nil
differently.
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 I understand it the old code was kind of implicit - it checks every visible argument, and if the argument was required but had no value provided, then argument.type.validate_input
would produce an error.
Now we're being a bit more explicit about generating a list of missing_required_inputs
. Rather than merging those explicit nils into the input just so we can call validate_input
on nil to generate the error... I think we could just generate the error instead of building missing_required_inputs
at all?
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.
generate the error instead of building
missing_required_inputs
Ah, yeah, fine with me. (And I guess it must have worked this way in the past, if it wasn't triggering .visible?
🤔 )
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.
generate the error instead of building
missing_required_inputs
In that case, I'm a bit unsure of how this should interact/overlap with the existing static validation of input object attributes.
A casual reading of that file initially suggested to me that this check would be performed earlier, removing the need to check for required + visible + not provided input arguments via GraphQL::Schema::InputObject.validate_non_null_input
.
On a related note: the following diff produces errors only in tests related to variable coercion, which makes me think that the passing tests for required input object arguments are hitting only the static validation checks.
diff
diff --git a/lib/graphql/schema/input_object.rb b/lib/graphql/schema/input_object.rb
index c4fb241fd..0a3a4c52d 100644
--- a/lib/graphql/schema/input_object.rb
+++ b/lib/graphql/schema/input_object.rb
@@ -181,16 +181,13 @@ module GraphQL
end
end
- # Inject missing required arguments
- missing_required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)|
+ self.arguments.each do |argument_name, argument|
if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
- m[argument_name] = nil
+ result.add_problem("TODO on #{self.graphql_name}", [argument_name])
end
-
- m
end
- input.merge(missing_required_inputs).each do |argument_name, value|
+ input.each do |argument_name, value|
argument = warden.get_argument(self, argument_name)
# Items in the input that are unexpected
unless argument
test output
Failure:
GraphQL::Query::Executor::variable coercion::for input objects with unknown keys in value#test_0001_returns a variable validation error [/Users/chris/src/github.com/grcooper/graphql-ruby/spec/graphql/query/executor_spec.rb:329]
Minitest::Assertion: --- expected
+++ actual
@@ -1,11 +1,10 @@
{"errors"=>
[{"message"=>
- "Variable $input of type [DairyProductInput] was provided invalid value for 0.foo (Field is not defined on DairyProductInput), 0.source (Expected value to not be null)",
+ "Variable $input of type [DairyProductInput] was provided invalid value for 0.source (TODO on DairyProductInput), 0.foo (Field is not defined on DairyProductInput)",
"locations"=>[{"line"=>1, "column"=>10}],
"extensions"=>
{"value"=>[{"foo"=>"bar"}],
"problems"=>
- [{"path"=>[0, "foo"],
- "explanation"=>"Field is not defined on DairyProductInput"},
- {"path"=>[0, "source"],
- "explanation"=>"Expected value to not be null"}]}}]}
+ [{"path"=>[0, "source"], "explanation"=>"TODO on DairyProductInput"},
+ {"path"=>[0, "foo"],
+ "explanation"=>"Field is not defined on DairyProductInput"}]}}]}
Failure:
GraphQL::Query::Executor::variable coercion::for required input object fields#test_0001_returns a variable validation error [/Users/chris/src/github.com/grcooper/graphql-ruby/spec/graphql/query/executor_spec.rb:306]
Minitest::Assertion: --- expected
+++ actual
@@ -1,9 +1 @@
-{"errors"=>
- [{"message"=>
- "Variable $input of type ReplaceValuesInput! was provided invalid value for values (Expected value to not be null)",
- "locations"=>[{"line"=>1, "column"=>13}],
- "extensions"=>
- {"value"=>{},
- "problems"=>
- [{"path"=>["values"],
- "explanation"=>"Expected value to not be null"}]}}]}
+#<GraphQL::Query::Result @query=... @to_h={"errors"=>[{"message"=>"Variable $input of type ReplaceValuesInput! was provided invalid value for values (TODO on ReplaceValuesInput)", "locations"=>[{"line"=>1, "column"=>13}], "extensions"=>{"value"=>{}, "problems"=>[{"path"=>["values"], "explanation"=>"TODO on ReplaceValuesInput"}]}}]}>
1602 tests, 7612 assertions, 2 failures, 0 errors, 0 skips
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 original comment here was an architecture/style question, not a behavioural one. It sounds like there's more we need to dig into here, but if we're confident that the current code works correctly could we get that out and then follow up on the other points?
👋 I'm taking over the PR as of today while Graham is 🌴 |
lib/graphql/schema/input_object.rb
Outdated
argument_result = argument.type.validate_input(input[name], ctx) | ||
if !argument_result.valid? | ||
result.merge_result!(name, argument_result) | ||
input.to_h.merge(required_inputs).each do |argument_name, value| |
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.
Trying this out in Shopify, input
is sometimes a ActionController::Parameters
instance, e.g. <ActionController::Parameters {"barcode"=>"53941047", "id"=>"gid://shopify/ProductVariant/30322695"} permitted: false>
.
This causes input.to_h
to raise ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
.
I notice that above this line we have this block:
begin
input.to_h
rescue
begin
# Handle ActionController::Parameters:
input.to_unsafe_h
rescue
# We're not sure it'll act like a hash, so reject it:
result.add_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) })
return result
end
end
Which doesn't actually convert input
if it's a ActionController::Parameters
. Should it do so?
Side question: Should we be permitting, to_h
'ing input
earlier?
Elsewhere in this gem, I see handling for this same scenario, checking if value.respond_to?(:to_unsafe_h)
.
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.
Previously, it required that .each
yielded key-value pairs. I think making a hash out of it is fine, if you have to, too 🤷
lib/graphql/schema/warden.rb
Outdated
@@ -106,6 +106,12 @@ def get_field(parent_type, field_name) | |||
@visible_parent_fields[parent_type][field_name] | |||
end | |||
|
|||
# @return [GraphQL::Argument, nil] The argument named `argument_name` on `parent_type`, if it exists and is visible | |||
def get_argument(parent_type, argument_name) | |||
argument = parent_type.arguments[argument_name] |
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.
#arguments
climbs the inheritance chain and merges argument definitions into a hash. When you only need one object, better to use .get_argument
which searches the chain until it finds a hit, and doesn't merge any hashes:
argument = parent_type.arguments[argument_name] | |
argument = parent_type.get_argument(argument_name) |
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 may be missing something, but I don't see get_argument
defined on GraphQL::Schema::InputObject
https://graphql-ruby.org/api-doc/1.10.11/GraphQL/Schema/InputObject.html
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.
Ah, sorry, I was thinking it would be around here, but I was mistaken:
graphql-ruby/lib/graphql/schema/member/has_arguments.rb
Lines 50 to 59 in 12947f4
# @return [Hash<String => GraphQL::Schema::Argument] Arguments defined on this thing, keyed by name. Includes inherited definitions | |
def arguments | |
inherited_arguments = ((self.is_a?(Class) && superclass.respond_to?(:arguments)) ? superclass.arguments : nil) | |
# Local definitions override inherited ones | |
if inherited_arguments | |
inherited_arguments.merge(own_arguments) | |
else | |
own_arguments | |
end | |
end |
It could be added like get_field:
graphql-ruby/lib/graphql/schema/member/has_fields.rb
Lines 29 to 40 in 12947f4
def get_field(field_name) | |
if (f = own_fields[field_name]) | |
f | |
else | |
for ancestor in ancestors | |
if ancestor.respond_to?(:own_fields) && f = ancestor.own_fields[field_name] | |
return f | |
end | |
end | |
nil | |
end | |
end |
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.
Good idea! Added :)
@@ -106,6 +106,12 @@ def get_field(parent_type, field_name) | |||
@visible_parent_fields[parent_type][field_name] | |||
end | |||
|
|||
# @return [GraphQL::Argument, nil] The argument named `argument_name` on `parent_type`, if it exists and is visible | |||
def get_argument(parent_type, argument_name) |
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.
Yeah, the call to the application's implementation would be cached, I don't know whether using read_through { ... }
here is worth it or not, no preference without actually seeing a benchmark.
end | ||
# Items in the input that are expected, but have invalid values | ||
argument_result = argument.type.validate_input(value, ctx) |
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.
adding them as nil
Do you mean, it worked this way in the old code? AFAIK the old code checked each argument and made sure that the provided value was valid, without special nil handling. I see that in the proposed change here, it has to handle nil
differently.
lib/graphql/schema/input_object.rb
Outdated
if visible_arguments_map[name].nil? | ||
result.add_problem("Field is not defined on #{self.graphql_name}", [name]) | ||
# Inject missing required arguments | ||
required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)| |
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.
self.arguments
can be expensive, for input objects that inherit arguments from their parent classes, because it merges hashes. In practice, it might not happen much, and the most likely case (no inherited arguments) is optimized to return own_arguments
. But, the nice part about warden.arguments(self)
here was that it was cached in any case. Not a show-stopper but thought I'd mention it in case it comes up later.
@@ -171,7 +171,7 @@ def validate_non_null_input(input, ctx) | |||
# We're not actually _using_ the coerced result, we're just | |||
# using these methods to make sure that the object will | |||
# behave like a hash below, when we call `each` on it. | |||
begin | |||
input = begin |
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 resolves #2985 (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.
The comment on this block is now wrong
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.
Good catch! 🔥
if visible_arguments_map[name].nil? | ||
result.add_problem("Field is not defined on #{self.graphql_name}", [name]) | ||
# Inject missing required arguments | ||
missing_required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)| |
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 hash has entirely nil values... I don't know if there's actually a nicer way to build it, or if we could get away with just an array of keys instead or something? but don't let it hold this up in the short term
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's some follow-up work to do but this solves our immediate problem so let's stop the bleeding.
61b2604
to
f2dcfa6
Compare
f2dcfa6
to
7408d45
Compare
Just shipped in 1.10.13! Thanks @grcooper for opening the followup issue. I'll add some thoughts there |
…arguments-visibility Only check used input arguments visibility
Replaces more usages of `warden.arguments` during static validation (since they trigger `visible?` calls for all arguments on a type regardless if they are specified or not) with more selective `warden.get_argument`. rmosolgo#2985 which fixed some of these occurrences, but not all of them. This also required additional backwards compatible work to port `get_argument` to the legacy types.
Replaces more usages of `warden.arguments` during static validation (since they trigger `visible?` calls for all arguments on a type regardless if they are specified or not) with more selective `warden.get_argument`. rmosolgo#2985 which fixed some of these occurrences, but not all of them. This also required additional backwards compatible work to port `get_argument` to the legacy types.
Replaces more usages of `warden.arguments` during static validation (since they trigger `visible?` calls for all arguments on a type regardless if they are specified or not) with more selective `warden.get_argument`. rmosolgo#2985 which fixed some of these occurrences, but not all of them. This also required additional backwards compatible work to port `get_argument` to the legacy types.
…arguments-visibility Only check used input arguments visibility
Replaces more usages of `warden.arguments` during static validation (since they trigger `visible?` calls for all arguments on a type regardless if they are specified or not) with more selective `warden.get_argument`. rmosolgo#2985 which fixed some of these occurrences, but not all of them. This also required additional backwards compatible work to port `get_argument` to the legacy types.
Replaces more usages of `warden.arguments` during static validation (since they trigger `visible?` calls for all arguments on a type regardless if they are specified or not) with more selective `warden.get_argument`. rmosolgo#2985 which fixed some of these occurrences, but not all of them. This also required additional backwards compatible work to port `get_argument` to the legacy types.
Replaces more usages of `warden.arguments` during static validation (since they trigger `visible?` calls for all arguments on a type regardless if they are specified or not) with more selective `warden.get_argument`. rmosolgo#2985 which fixed some of these occurrences, but not all of them. This also required additional backwards compatible work to port `get_argument` to the legacy types.
This line:
visible_arguments_map = warden.arguments(self).reduce({}) { |m, f| m[f.name] = f; m}
Was straightforward, but was also making visibility calls to arguments that we did not need to check.
I have sacrificed a small bit of readability here in order to reduce the number of
visible?
calls being made in warden.There are three steps to this process to make sure it remained the same and didn't break tests:
nil
that were missing from input. This is to satisfy this test:graphql-ruby/spec/graphql/query/executor_spec.rb
Line 293 in 39b78e5
graphql-ruby/spec/graphql/query/executor_spec.rb
Line 313 in 39b78e5