graphql 1.11.7 → 1.12.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +7 -5
  3. data/lib/generators/graphql/relay.rb +55 -0
  4. data/lib/generators/graphql/relay_generator.rb +20 -0
  5. data/lib/generators/graphql/templates/base_connection.erb +8 -0
  6. data/lib/generators/graphql/templates/base_edge.erb +8 -0
  7. data/lib/generators/graphql/templates/node_type.erb +9 -0
  8. data/lib/generators/graphql/templates/object.erb +1 -1
  9. data/lib/generators/graphql/templates/query_type.erb +1 -3
  10. data/lib/generators/graphql/templates/schema.erb +8 -35
  11. data/lib/graphql.rb +38 -4
  12. data/lib/graphql/analysis/analyze_query.rb +7 -0
  13. data/lib/graphql/analysis/ast.rb +11 -2
  14. data/lib/graphql/analysis/ast/visitor.rb +9 -1
  15. data/lib/graphql/backtrace.rb +28 -19
  16. data/lib/graphql/backtrace/inspect_result.rb +0 -1
  17. data/lib/graphql/backtrace/legacy_tracer.rb +56 -0
  18. data/lib/graphql/backtrace/table.rb +22 -3
  19. data/lib/graphql/backtrace/traced_error.rb +0 -1
  20. data/lib/graphql/backtrace/tracer.rb +37 -10
  21. data/lib/graphql/backwards_compatibility.rb +2 -1
  22. data/lib/graphql/base_type.rb +1 -1
  23. data/lib/graphql/compatibility/execution_specification.rb +1 -0
  24. data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
  25. data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
  26. data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
  27. data/lib/graphql/dataloader.rb +208 -0
  28. data/lib/graphql/dataloader/null_dataloader.rb +21 -0
  29. data/lib/graphql/dataloader/request.rb +19 -0
  30. data/lib/graphql/dataloader/request_all.rb +19 -0
  31. data/lib/graphql/dataloader/source.rb +107 -0
  32. data/lib/graphql/define/assign_global_id_field.rb +1 -1
  33. data/lib/graphql/define/instance_definable.rb +32 -2
  34. data/lib/graphql/define/type_definer.rb +5 -5
  35. data/lib/graphql/deprecated_dsl.rb +7 -2
  36. data/lib/graphql/deprecation.rb +13 -0
  37. data/lib/graphql/enum_type.rb +2 -0
  38. data/lib/graphql/execution/errors.rb +4 -0
  39. data/lib/graphql/execution/execute.rb +7 -0
  40. data/lib/graphql/execution/interpreter.rb +11 -7
  41. data/lib/graphql/execution/interpreter/arguments.rb +51 -14
  42. data/lib/graphql/execution/interpreter/arguments_cache.rb +37 -14
  43. data/lib/graphql/execution/interpreter/handles_raw_value.rb +0 -7
  44. data/lib/graphql/execution/interpreter/resolve.rb +33 -25
  45. data/lib/graphql/execution/interpreter/runtime.rb +173 -123
  46. data/lib/graphql/execution/multiplex.rb +36 -23
  47. data/lib/graphql/function.rb +4 -0
  48. data/lib/graphql/input_object_type.rb +2 -0
  49. data/lib/graphql/interface_type.rb +3 -1
  50. data/lib/graphql/internal_representation/document.rb +2 -2
  51. data/lib/graphql/internal_representation/rewrite.rb +1 -1
  52. data/lib/graphql/language/document_from_schema_definition.rb +50 -23
  53. data/lib/graphql/object_type.rb +2 -2
  54. data/lib/graphql/pagination/connection.rb +5 -1
  55. data/lib/graphql/pagination/connections.rb +6 -16
  56. data/lib/graphql/parse_error.rb +0 -1
  57. data/lib/graphql/query.rb +10 -2
  58. data/lib/graphql/query/arguments.rb +1 -1
  59. data/lib/graphql/query/arguments_cache.rb +0 -1
  60. data/lib/graphql/query/context.rb +4 -2
  61. data/lib/graphql/query/executor.rb +0 -1
  62. data/lib/graphql/query/null_context.rb +3 -2
  63. data/lib/graphql/query/serial_execution.rb +1 -0
  64. data/lib/graphql/query/variable_validation_error.rb +1 -1
  65. data/lib/graphql/relay/base_connection.rb +7 -0
  66. data/lib/graphql/relay/connection_instrumentation.rb +4 -4
  67. data/lib/graphql/relay/connection_type.rb +1 -1
  68. data/lib/graphql/relay/mutation.rb +1 -0
  69. data/lib/graphql/relay/node.rb +3 -0
  70. data/lib/graphql/relay/type_extensions.rb +2 -0
  71. data/lib/graphql/scalar_type.rb +2 -0
  72. data/lib/graphql/schema.rb +64 -26
  73. data/lib/graphql/schema/argument.rb +86 -7
  74. data/lib/graphql/schema/build_from_definition.rb +139 -51
  75. data/lib/graphql/schema/directive.rb +76 -0
  76. data/lib/graphql/schema/directive/flagged.rb +57 -0
  77. data/lib/graphql/schema/enum.rb +3 -0
  78. data/lib/graphql/schema/enum_value.rb +12 -6
  79. data/lib/graphql/schema/field.rb +40 -16
  80. data/lib/graphql/schema/field/connection_extension.rb +3 -2
  81. data/lib/graphql/schema/find_inherited_value.rb +3 -1
  82. data/lib/graphql/schema/input_object.rb +39 -24
  83. data/lib/graphql/schema/interface.rb +1 -0
  84. data/lib/graphql/schema/member.rb +4 -0
  85. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -0
  86. data/lib/graphql/schema/member/build_type.rb +3 -3
  87. data/lib/graphql/schema/member/has_arguments.rb +54 -49
  88. data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
  89. data/lib/graphql/schema/member/has_directives.rb +98 -0
  90. data/lib/graphql/schema/member/has_fields.rb +1 -4
  91. data/lib/graphql/schema/member/has_validators.rb +31 -0
  92. data/lib/graphql/schema/member/instrumentation.rb +0 -1
  93. data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
  94. data/lib/graphql/schema/middleware_chain.rb +1 -1
  95. data/lib/graphql/schema/object.rb +11 -0
  96. data/lib/graphql/schema/printer.rb +5 -4
  97. data/lib/graphql/schema/resolver.rb +7 -0
  98. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -0
  99. data/lib/graphql/schema/subscription.rb +19 -1
  100. data/lib/graphql/schema/timeout_middleware.rb +3 -1
  101. data/lib/graphql/schema/validation.rb +4 -2
  102. data/lib/graphql/schema/validator.rb +163 -0
  103. data/lib/graphql/schema/validator/exclusion_validator.rb +31 -0
  104. data/lib/graphql/schema/validator/format_validator.rb +49 -0
  105. data/lib/graphql/schema/validator/inclusion_validator.rb +33 -0
  106. data/lib/graphql/schema/validator/length_validator.rb +57 -0
  107. data/lib/graphql/schema/validator/numericality_validator.rb +71 -0
  108. data/lib/graphql/schema/validator/required_validator.rb +68 -0
  109. data/lib/graphql/static_validation/validator.rb +4 -0
  110. data/lib/graphql/subscriptions.rb +17 -20
  111. data/lib/graphql/subscriptions/event.rb +0 -1
  112. data/lib/graphql/subscriptions/instrumentation.rb +0 -1
  113. data/lib/graphql/subscriptions/serialize.rb +0 -1
  114. data/lib/graphql/subscriptions/subscription_root.rb +1 -1
  115. data/lib/graphql/tracing.rb +2 -2
  116. data/lib/graphql/tracing/appoptics_tracing.rb +3 -1
  117. data/lib/graphql/tracing/platform_tracing.rb +3 -1
  118. data/lib/graphql/tracing/skylight_tracing.rb +1 -1
  119. data/lib/graphql/types/relay.rb +11 -3
  120. data/lib/graphql/types/relay/base_connection.rb +2 -92
  121. data/lib/graphql/types/relay/base_edge.rb +2 -35
  122. data/lib/graphql/types/relay/connection_behaviors.rb +123 -0
  123. data/lib/graphql/types/relay/default_relay.rb +27 -0
  124. data/lib/graphql/types/relay/edge_behaviors.rb +42 -0
  125. data/lib/graphql/types/relay/has_node_field.rb +41 -0
  126. data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
  127. data/lib/graphql/types/relay/node.rb +2 -4
  128. data/lib/graphql/types/relay/node_behaviors.rb +15 -0
  129. data/lib/graphql/types/relay/node_field.rb +1 -19
  130. data/lib/graphql/types/relay/nodes_field.rb +1 -19
  131. data/lib/graphql/types/relay/page_info.rb +2 -14
  132. data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
  133. data/lib/graphql/union_type.rb +2 -0
  134. data/lib/graphql/upgrader/member.rb +1 -0
  135. data/lib/graphql/upgrader/schema.rb +1 -0
  136. data/lib/graphql/version.rb +1 -1
  137. metadata +50 -93
  138. data/lib/graphql/types/relay/base_field.rb +0 -22
  139. data/lib/graphql/types/relay/base_interface.rb +0 -29
  140. data/lib/graphql/types/relay/base_object.rb +0 -26
@@ -6,17 +6,21 @@ module GraphQL
6
6
  class ArgumentsCache
7
7
  def initialize(query)
8
8
  @query = query
9
+ @dataloader = query.context.dataloader
9
10
  @storage = Hash.new do |h, ast_node|
10
11
  h[ast_node] = Hash.new do |h2, arg_owner|
11
12
  h2[arg_owner] = Hash.new do |h3, parent_object|
12
- # First, normalize all AST or Ruby values to a plain Ruby hash
13
- args_hash = prepare_args_hash(ast_node)
14
- # Then call into the schema to coerce those incoming values
15
- args = arg_owner.coerce_arguments(parent_object, args_hash, query.context)
13
+ dataload_for(ast_node, arg_owner, parent_object) do |kwarg_arguments|
14
+ h3[parent_object] = @query.schema.after_lazy(kwarg_arguments) do |resolved_args|
15
+ h3[parent_object] = resolved_args
16
+ end
17
+ end
16
18
 
17
- h3[parent_object] = @query.schema.after_lazy(args) do |resolved_args|
18
- # when this promise is resolved, update the cache with the resolved value
19
- h3[parent_object] = resolved_args
19
+ if !h3.key?(parent_object)
20
+ # TODO should i bother putting anything here?
21
+ h3[parent_object] = NO_ARGUMENTS
22
+ else
23
+ h3[parent_object]
20
24
  end
21
25
  end
22
26
  end
@@ -25,6 +29,25 @@ module GraphQL
25
29
 
26
30
  def fetch(ast_node, argument_owner, parent_object)
27
31
  @storage[ast_node][argument_owner][parent_object]
32
+ # If any jobs were enqueued, run them now,
33
+ # since this might have been called outside of execution.
34
+ # (The jobs are responsible for updating `result` in-place.)
35
+ @dataloader.run
36
+ # Ack, the _hash_ is updated, but the key is eventually
37
+ # overridden with an immutable arguments instance.
38
+ # The first call queues up the job,
39
+ # then this call fetches the result.
40
+ # TODO this should be better, find a solution
41
+ # that works with merging the runtime.rb code
42
+ @storage[ast_node][argument_owner][parent_object]
43
+ end
44
+
45
+ # @yield [Interpreter::Arguments, Lazy<Interpreter::Arguments>] The finally-loaded arguments
46
+ def dataload_for(ast_node, argument_owner, parent_object, &block)
47
+ # First, normalize all AST or Ruby values to a plain Ruby hash
48
+ args_hash = self.class.prepare_args_hash(@query, ast_node)
49
+ argument_owner.coerce_arguments(parent_object, args_hash, @query.context, &block)
50
+ nil
28
51
  end
29
52
 
30
53
  private
@@ -33,7 +56,7 @@ module GraphQL
33
56
 
34
57
  NO_VALUE_GIVEN = Object.new
35
58
 
36
- def prepare_args_hash(ast_arg_or_hash_or_value)
59
+ def self.prepare_args_hash(query, ast_arg_or_hash_or_value)
37
60
  case ast_arg_or_hash_or_value
38
61
  when Hash
39
62
  if ast_arg_or_hash_or_value.empty?
@@ -41,27 +64,27 @@ module GraphQL
41
64
  end
42
65
  args_hash = {}
43
66
  ast_arg_or_hash_or_value.each do |k, v|
44
- args_hash[k] = prepare_args_hash(v)
67
+ args_hash[k] = prepare_args_hash(query, v)
45
68
  end
46
69
  args_hash
47
70
  when Array
48
- ast_arg_or_hash_or_value.map { |v| prepare_args_hash(v) }
71
+ ast_arg_or_hash_or_value.map { |v| prepare_args_hash(query, v) }
49
72
  when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive
50
73
  if ast_arg_or_hash_or_value.arguments.empty?
51
74
  return NO_ARGUMENTS
52
75
  end
53
76
  args_hash = {}
54
77
  ast_arg_or_hash_or_value.arguments.each do |arg|
55
- v = prepare_args_hash(arg.value)
78
+ v = prepare_args_hash(query, arg.value)
56
79
  if v != NO_VALUE_GIVEN
57
80
  args_hash[arg.name] = v
58
81
  end
59
82
  end
60
83
  args_hash
61
84
  when GraphQL::Language::Nodes::VariableIdentifier
62
- if @query.variables.key?(ast_arg_or_hash_or_value.name)
63
- variable_value = @query.variables[ast_arg_or_hash_or_value.name]
64
- prepare_args_hash(variable_value)
85
+ if query.variables.key?(ast_arg_or_hash_or_value.name)
86
+ variable_value = query.variables[ast_arg_or_hash_or_value.name]
87
+ prepare_args_hash(query, variable_value)
65
88
  else
66
89
  NO_VALUE_GIVEN
67
90
  end
@@ -13,13 +13,6 @@ module GraphQL
13
13
  @object
14
14
  end
15
15
  end
16
-
17
- # Allows to return "raw" value from the resolver
18
- module HandlesRawValue
19
- def raw_value(obj)
20
- RawValue.new(obj)
21
- end
22
- end
23
16
  end
24
17
  end
25
18
  end
@@ -6,10 +6,9 @@ module GraphQL
6
6
  module Resolve
7
7
  # Continue field results in `results` until there's nothing else to continue.
8
8
  # @return [void]
9
- def self.resolve_all(results)
10
- while results.any?
11
- results = resolve(results)
12
- end
9
+ def self.resolve_all(results, dataloader)
10
+ dataloader.append_job { resolve(results, dataloader) }
11
+ nil
13
12
  end
14
13
 
15
14
  # After getting `results` back from an interpreter evaluation,
@@ -24,33 +23,42 @@ module GraphQL
24
23
  # return {Lazy} instances if there's more work to be done,
25
24
  # or return {Hash}/{Array} if the query should be continued.
26
25
  #
27
- # @param results [Array]
28
- # @return [Array] Same size, filled with finished values
29
- def self.resolve(results)
26
+ # @return [void]
27
+ def self.resolve(results, dataloader)
28
+ # There might be pending jobs here that _will_ write lazies
29
+ # into the result hash. We should run them out, so we
30
+ # can be sure that all lazies will be present in the result hashes.
31
+ # A better implementation would somehow interleave (or unify)
32
+ # these approaches.
33
+ dataloader.run
30
34
  next_results = []
31
-
32
- # Work through the queue until it's empty
33
- while results.size > 0
35
+ while results.any?
34
36
  result_value = results.shift
35
-
36
- if result_value.is_a?(Lazy)
37
- result_value = result_value.value
38
- end
39
-
40
- if result_value.is_a?(Lazy)
41
- # Since this field returned another lazy,
42
- # add it to the same queue
43
- results << result_value
44
- elsif result_value.is_a?(Hash)
45
- # This is part of the next level, add it
46
- next_results.concat(result_value.values)
37
+ if result_value.is_a?(Hash)
38
+ results.concat(result_value.values)
39
+ next
47
40
  elsif result_value.is_a?(Array)
48
- # This is part of the next level, add it
49
- next_results.concat(result_value)
41
+ results.concat(result_value)
42
+ next
43
+ elsif result_value.is_a?(Lazy)
44
+ loaded_value = result_value.value
45
+ if loaded_value.is_a?(Lazy)
46
+ # Since this field returned another lazy,
47
+ # add it to the same queue
48
+ results << loaded_value
49
+ elsif loaded_value.is_a?(Hash) || loaded_value.is_a?(Array)
50
+ # Add these values in wholesale --
51
+ # they might be modified by later work in the dataloader.
52
+ next_results << loaded_value
53
+ end
50
54
  end
51
55
  end
52
56
 
53
- next_results
57
+ if next_results.any?
58
+ dataloader.append_job { resolve(next_results, dataloader) }
59
+ end
60
+
61
+ nil
54
62
  end
55
63
  end
56
64
  end
@@ -19,8 +19,10 @@ module GraphQL
19
19
 
20
20
  def initialize(query:, response:)
21
21
  @query = query
22
+ @dataloader = query.multiplex.dataloader
22
23
  @schema = query.schema
23
24
  @context = query.context
25
+ @multiplex_context = query.multiplex.context
24
26
  @interpreter_context = @context.namespace(:interpreter)
25
27
  @response = response
26
28
  @dead_paths = {}
@@ -54,7 +56,18 @@ module GraphQL
54
56
  # Root .authorized? returned false.
55
57
  write_in_response(path, nil)
56
58
  else
57
- evaluate_selections(path, context.scoped_context, object_proxy, root_type, root_operation.selections, root_operation_type: root_op_type)
59
+ gathered_selections = gather_selections(object_proxy, root_type, root_operation.selections)
60
+ # Make the first fiber which will begin execution
61
+ @dataloader.append_job {
62
+ evaluate_selections(
63
+ path,
64
+ context.scoped_context,
65
+ object_proxy,
66
+ root_type,
67
+ root_op_type == "mutation",
68
+ gathered_selections,
69
+ )
70
+ }
58
71
  end
59
72
  delete_interpreter_context(:current_path)
60
73
  delete_interpreter_context(:current_field)
@@ -63,7 +76,7 @@ module GraphQL
63
76
  nil
64
77
  end
65
78
 
66
- def gather_selections(owner_object, owner_type, selections, selections_by_name)
79
+ def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
67
80
  selections.each do |node|
68
81
  # Skip gathering this if the directive says so
69
82
  if !directives_include?(node, owner_object, owner_type)
@@ -115,144 +128,172 @@ module GraphQL
115
128
  raise "Invariant: unexpected selection class: #{node.class}"
116
129
  end
117
130
  end
131
+ selections_by_name
118
132
  end
119
133
 
120
134
  NO_ARGS = {}.freeze
121
135
 
122
- def evaluate_selections(path, scoped_context, owner_object, owner_type, selections, root_operation_type: nil)
136
+ # @return [void]
137
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection, gathered_selections)
123
138
  set_all_interpreter_context(owner_object, nil, nil, path)
124
- selections_by_name = {}
125
- gather_selections(owner_object, owner_type, selections, selections_by_name)
126
- selections_by_name.each do |result_name, field_ast_nodes_or_ast_node|
127
- # As a performance optimization, the hash key will be a `Node` if
128
- # there's only one selection of the field. But if there are multiple
129
- # selections of the field, it will be an Array of nodes
130
- if field_ast_nodes_or_ast_node.is_a?(Array)
131
- field_ast_nodes = field_ast_nodes_or_ast_node
132
- ast_node = field_ast_nodes.first
139
+
140
+ gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
141
+ @dataloader.append_job {
142
+ evaluate_selection(
143
+ path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection
144
+ )
145
+ }
146
+ end
147
+
148
+ nil
149
+ end
150
+
151
+ attr_reader :progress_path
152
+
153
+ # @return [void]
154
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
155
+ # As a performance optimization, the hash key will be a `Node` if
156
+ # there's only one selection of the field. But if there are multiple
157
+ # selections of the field, it will be an Array of nodes
158
+ if field_ast_nodes_or_ast_node.is_a?(Array)
159
+ field_ast_nodes = field_ast_nodes_or_ast_node
160
+ ast_node = field_ast_nodes.first
161
+ else
162
+ field_ast_nodes = nil
163
+ ast_node = field_ast_nodes_or_ast_node
164
+ end
165
+ field_name = ast_node.name
166
+ field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
167
+ is_introspection = false
168
+ if field_defn.nil?
169
+ field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
170
+ is_introspection = true
171
+ entry_point_field
172
+ elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
173
+ is_introspection = true
174
+ dynamic_field
133
175
  else
134
- field_ast_nodes = nil
135
- ast_node = field_ast_nodes_or_ast_node
176
+ raise "Invariant: no field for #{owner_type}.#{field_name}"
136
177
  end
137
- field_name = ast_node.name
138
- field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
139
- is_introspection = false
140
- if field_defn.nil?
141
- field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
142
- is_introspection = true
143
- entry_point_field
144
- elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
145
- is_introspection = true
146
- dynamic_field
147
- else
148
- raise "Invariant: no field for #{owner_type}.#{field_name}"
149
- end
150
- end
151
- return_type = field_defn.type
178
+ end
179
+ return_type = field_defn.type
152
180
 
153
- next_path = path.dup
154
- next_path << result_name
155
- next_path.freeze
181
+ next_path = path.dup
182
+ next_path << result_name
183
+ next_path.freeze
156
184
 
157
- # This seems janky, but we need to know
158
- # the field's return type at this path in order
159
- # to propagate `null`
160
- set_type_at_path(next_path, return_type)
161
- # Set this before calling `run_with_directives`, so that the directive can have the latest path
162
- set_all_interpreter_context(nil, field_defn, nil, next_path)
185
+ # This seems janky, but we need to know
186
+ # the field's return type at this path in order
187
+ # to propagate `null`
188
+ set_type_at_path(next_path, return_type)
189
+ # Set this before calling `run_with_directives`, so that the directive can have the latest path
190
+ set_all_interpreter_context(nil, field_defn, nil, next_path)
163
191
 
164
- context.scoped_context = scoped_context
165
- object = owner_object
192
+ context.scoped_context = scoped_context
193
+ object = owner_object
166
194
 
167
- if is_introspection
168
- object = authorized_new(field_defn.owner, object, context, next_path)
195
+ if is_introspection
196
+ object = authorized_new(field_defn.owner, object, context, next_path)
197
+ end
198
+
199
+ total_args_count = field_defn.arguments.size
200
+ if total_args_count == 0
201
+ kwarg_arguments = GraphQL::Execution::Interpreter::Arguments::EMPTY
202
+ evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
203
+ else
204
+ # TODO remove all arguments(...) usages?
205
+ @query.arguments_cache.dataload_for(ast_node, field_defn, object) do |resolved_arguments|
206
+ evaluate_selection_with_args(resolved_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field)
169
207
  end
208
+ end
209
+ end
170
210
 
171
- begin
172
- kwarg_arguments = arguments(object, field_defn, ast_node)
173
- rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => e
174
- continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
211
+ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_node, field_ast_nodes, scoped_context, owner_type, object, is_eager_field) # rubocop:disable Metrics/ParameterLists
212
+ context.scoped_context = scoped_context
213
+ return_type = field_defn.type
214
+ after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
215
+ if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
216
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
175
217
  next
176
218
  end
177
219
 
178
- after_lazy(kwarg_arguments, owner: owner_type, field: field_defn, path: next_path, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |resolved_arguments|
179
- case resolved_arguments
180
- when GraphQL::ExecutionError, GraphQL::UnauthorizedError
181
- continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
182
- next
183
- end
184
-
185
- if resolved_arguments.empty? && field_defn.extras.empty?
186
- # We can avoid allocating the `{ Symbol => Object }` hash in this case
187
- kwarg_arguments = NO_ARGS
188
- else
189
- kwarg_arguments = resolved_arguments.keyword_arguments
190
-
191
- field_defn.extras.each do |extra|
192
- case extra
193
- when :ast_node
194
- kwarg_arguments[:ast_node] = ast_node
195
- when :execution_errors
196
- kwarg_arguments[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
197
- when :path
198
- kwarg_arguments[:path] = next_path
199
- when :lookahead
200
- if !field_ast_nodes
201
- field_ast_nodes = [ast_node]
202
- end
203
- kwarg_arguments[:lookahead] = Execution::Lookahead.new(
204
- query: query,
205
- ast_nodes: field_ast_nodes,
206
- field: field_defn,
207
- )
208
- when :argument_details
209
- kwarg_arguments[:argument_details] = resolved_arguments
210
- else
211
- kwarg_arguments[extra] = field_defn.fetch_extra(extra, context)
220
+ kwarg_arguments = if resolved_arguments.empty? && field_defn.extras.empty?
221
+ # We can avoid allocating the `{ Symbol => Object }` hash in this case
222
+ NO_ARGS
223
+ else
224
+ # Bundle up the extras, then make a new arguments instance
225
+ # that includes the extras, too.
226
+ extra_args = {}
227
+ field_defn.extras.each do |extra|
228
+ case extra
229
+ when :ast_node
230
+ extra_args[:ast_node] = ast_node
231
+ when :execution_errors
232
+ extra_args[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
233
+ when :path
234
+ extra_args[:path] = next_path
235
+ when :lookahead
236
+ if !field_ast_nodes
237
+ field_ast_nodes = [ast_node]
212
238
  end
239
+
240
+ extra_args[:lookahead] = Execution::Lookahead.new(
241
+ query: query,
242
+ ast_nodes: field_ast_nodes,
243
+ field: field_defn,
244
+ )
245
+ when :argument_details
246
+ # Use this flag to tell Interpreter::Arguments to add itself
247
+ # to the keyword args hash _before_ freezing everything.
248
+ extra_args[:argument_details] = :__arguments_add_self
249
+ else
250
+ extra_args[extra] = field_defn.fetch_extra(extra, context)
213
251
  end
214
252
  end
253
+ resolved_arguments = resolved_arguments.merge_extras(extra_args)
254
+ resolved_arguments.keyword_arguments
255
+ end
215
256
 
216
- set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
257
+ set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
217
258
 
218
- # Optimize for the case that field is selected only once
219
- if field_ast_nodes.nil? || field_ast_nodes.size == 1
220
- next_selections = ast_node.selections
221
- else
222
- next_selections = []
223
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
224
- end
259
+ # Optimize for the case that field is selected only once
260
+ if field_ast_nodes.nil? || field_ast_nodes.size == 1
261
+ next_selections = ast_node.selections
262
+ else
263
+ next_selections = []
264
+ field_ast_nodes.each { |f| next_selections.concat(f.selections) }
265
+ end
225
266
 
226
- field_result = resolve_with_directives(object, ast_node) do
227
- # Actually call the field resolver and capture the result
228
- app_result = begin
229
- query.with_error_handling do
230
- query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, query: query, object: object, arguments: kwarg_arguments}) do
231
- field_defn.resolve(object, kwarg_arguments, context)
232
- end
267
+ field_result = resolve_with_directives(object, ast_node) do
268
+ # Actually call the field resolver and capture the result
269
+ app_result = begin
270
+ query.with_error_handling do
271
+ query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, query: query, object: object, arguments: kwarg_arguments}) do
272
+ field_defn.resolve(object, kwarg_arguments, context)
233
273
  end
234
- rescue GraphQL::ExecutionError => err
235
- err
236
274
  end
237
- after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
238
- continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
239
- if RawValue === continue_value
240
- # Write raw value directly to the response without resolving nested objects
241
- write_in_response(next_path, continue_value.resolve)
242
- elsif HALT != continue_value
243
- continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
244
- end
275
+ rescue GraphQL::ExecutionError => err
276
+ err
277
+ end
278
+ after_lazy(app_result, owner: owner_type, field: field_defn, path: next_path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: object, arguments: kwarg_arguments) do |inner_result|
279
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
280
+ if RawValue === continue_value
281
+ # Write raw value directly to the response without resolving nested objects
282
+ write_in_response(next_path, continue_value.resolve)
283
+ elsif HALT != continue_value
284
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
245
285
  end
246
286
  end
287
+ end
247
288
 
248
- # If this field is a root mutation field, immediately resolve
249
- # all of its child fields before moving on to the next root mutation field.
250
- # (Subselections of this mutation will still be resolved level-by-level.)
251
- if root_operation_type == "mutation"
252
- Interpreter::Resolve.resolve_all([field_result])
253
- else
254
- field_result
255
- end
289
+ # If this field is a root mutation field, immediately resolve
290
+ # all of its child fields before moving on to the next root mutation field.
291
+ # (Subselections of this mutation will still be resolved level-by-level.)
292
+ if is_eager_field
293
+ Interpreter::Resolve.resolve_all([field_result], @dataloader)
294
+ else
295
+ # Return this from `after_lazy` because it might be another lazy that needs to be resolved
296
+ field_result
256
297
  end
257
298
  end
258
299
  end
@@ -314,7 +355,7 @@ module GraphQL
314
355
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
315
356
  resolved_value ||= value
316
357
 
317
- after_lazy(resolved_type_or_lazy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
358
+ after_lazy(resolved_type_or_lazy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |resolved_type|
318
359
  possible_types = query.possible_types(current_type)
319
360
 
320
361
  if !possible_types.include?(resolved_type)
@@ -334,12 +375,13 @@ module GraphQL
334
375
  rescue GraphQL::ExecutionError => err
335
376
  err
336
377
  end
337
- after_lazy(object_proxy, owner: current_type, path: path, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
378
+ after_lazy(object_proxy, owner: current_type, path: path, ast_node: ast_node, scoped_context: context.scoped_context, field: field, owner_object: owner_object, arguments: arguments, trace: false) do |inner_object|
338
379
  continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
339
380
  if HALT != continue_value
340
381
  response_hash = {}
341
382
  write_in_response(path, response_hash)
342
- evaluate_selections(path, context.scoped_context, continue_value, current_type, next_selections)
383
+ gathered_selections = gather_selections(continue_value, current_type, next_selections)
384
+ evaluate_selections(path, context.scoped_context, continue_value, current_type, false, gathered_selections)
343
385
  response_hash
344
386
  end
345
387
  end
@@ -357,7 +399,7 @@ module GraphQL
357
399
  idx += 1
358
400
  set_type_at_path(next_path, inner_type)
359
401
  # This will update `response_list` with the lazy
360
- after_lazy(inner_value, owner: inner_type, path: next_path, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
402
+ after_lazy(inner_value, owner: inner_type, path: next_path, ast_node: ast_node, scoped_context: scoped_context, field: field, owner_object: owner_object, arguments: arguments) do |inner_inner_value|
361
403
  continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
362
404
  if HALT != continue_value
363
405
  continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
@@ -440,7 +482,7 @@ module GraphQL
440
482
  # @param eager [Boolean] Set to `true` for mutation root fields only
441
483
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
442
484
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
443
- def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, eager: false, trace: true, &block)
485
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
444
486
  set_all_interpreter_context(owner_object, field, arguments, path)
445
487
  if schema.lazy?(lazy_obj)
446
488
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
@@ -451,7 +493,7 @@ module GraphQL
451
493
  inner_obj = begin
452
494
  query.with_error_handling do
453
495
  if trace
454
- query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments}) do
496
+ query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
455
497
  schema.sync_lazy(lazy_obj)
456
498
  end
457
499
  else
@@ -461,7 +503,7 @@ module GraphQL
461
503
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
462
504
  err
463
505
  end
464
- after_lazy(inner_obj, owner: owner, field: field, path: path, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager, trace: trace, &block)
506
+ after_lazy(inner_obj, owner: owner, field: field, path: path, ast_node: ast_node, scoped_context: context.scoped_context, owner_object: owner_object, arguments: arguments, eager: eager, trace: trace, &block)
465
507
  end
466
508
 
467
509
  if eager
@@ -476,9 +518,7 @@ module GraphQL
476
518
  end
477
519
 
478
520
  def arguments(graphql_object, arg_owner, ast_node)
479
- # Don't cache arguments if field extras or extensions are requested since they can mutate the argument data structure
480
- if arg_owner.arguments_statically_coercible? &&
481
- (!arg_owner.is_a?(GraphQL::Schema::Field) || (arg_owner.extras.empty? && arg_owner.extensions.empty?))
521
+ if arg_owner.arguments_statically_coercible?
482
522
  query.arguments_for(ast_node, arg_owner)
483
523
  else
484
524
  # The arguments must be prepared in the context of the given object
@@ -519,6 +559,16 @@ module GraphQL
519
559
  end
520
560
  end
521
561
 
562
+ def value_at(path)
563
+ i = 0
564
+ value = @response.final_value
565
+ while value && (part = path[i])
566
+ value = value[part]
567
+ i += 1
568
+ end
569
+ value
570
+ end
571
+
522
572
  # To propagate nulls, we have to know what the field type was
523
573
  # at previous parts of the response.
524
574
  # This hash matches the response