graphql 1.11.5 → 1.12.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +5 -5
  3. data/lib/generators/graphql/object_generator.rb +2 -0
  4. data/lib/generators/graphql/relay_generator.rb +63 -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 +39 -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/legacy_tracer.rb +56 -0
  17. data/lib/graphql/backtrace/table.rb +22 -2
  18. data/lib/graphql/backtrace/tracer.rb +40 -9
  19. data/lib/graphql/backwards_compatibility.rb +2 -1
  20. data/lib/graphql/base_type.rb +1 -1
  21. data/lib/graphql/compatibility/execution_specification.rb +1 -0
  22. data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
  23. data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
  24. data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
  25. data/lib/graphql/dataloader.rb +198 -0
  26. data/lib/graphql/dataloader/null_dataloader.rb +21 -0
  27. data/lib/graphql/dataloader/request.rb +24 -0
  28. data/lib/graphql/dataloader/request_all.rb +22 -0
  29. data/lib/graphql/dataloader/source.rb +93 -0
  30. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  31. data/lib/graphql/define/instance_definable.rb +32 -2
  32. data/lib/graphql/define/type_definer.rb +5 -5
  33. data/lib/graphql/deprecated_dsl.rb +7 -2
  34. data/lib/graphql/deprecation.rb +13 -0
  35. data/lib/graphql/enum_type.rb +2 -0
  36. data/lib/graphql/execution/errors.rb +4 -0
  37. data/lib/graphql/execution/execute.rb +7 -0
  38. data/lib/graphql/execution/interpreter.rb +10 -6
  39. data/lib/graphql/execution/interpreter/arguments.rb +57 -5
  40. data/lib/graphql/execution/interpreter/arguments_cache.rb +8 -0
  41. data/lib/graphql/execution/interpreter/handles_raw_value.rb +0 -7
  42. data/lib/graphql/execution/interpreter/runtime.rb +219 -117
  43. data/lib/graphql/execution/multiplex.rb +20 -6
  44. data/lib/graphql/function.rb +4 -0
  45. data/lib/graphql/input_object_type.rb +2 -0
  46. data/lib/graphql/integer_decoding_error.rb +17 -0
  47. data/lib/graphql/interface_type.rb +3 -1
  48. data/lib/graphql/internal_representation/document.rb +2 -2
  49. data/lib/graphql/internal_representation/rewrite.rb +1 -1
  50. data/lib/graphql/invalid_null_error.rb +1 -1
  51. data/lib/graphql/language/document_from_schema_definition.rb +50 -23
  52. data/lib/graphql/object_type.rb +2 -0
  53. data/lib/graphql/pagination/connection.rb +5 -1
  54. data/lib/graphql/pagination/connections.rb +15 -19
  55. data/lib/graphql/query.rb +6 -1
  56. data/lib/graphql/query/arguments.rb +1 -1
  57. data/lib/graphql/query/context.rb +8 -1
  58. data/lib/graphql/query/serial_execution.rb +1 -0
  59. data/lib/graphql/query/validation_pipeline.rb +1 -1
  60. data/lib/graphql/relay/array_connection.rb +2 -2
  61. data/lib/graphql/relay/base_connection.rb +7 -0
  62. data/lib/graphql/relay/connection_instrumentation.rb +4 -4
  63. data/lib/graphql/relay/connection_type.rb +1 -1
  64. data/lib/graphql/relay/mutation.rb +1 -0
  65. data/lib/graphql/relay/node.rb +3 -0
  66. data/lib/graphql/relay/range_add.rb +14 -5
  67. data/lib/graphql/relay/type_extensions.rb +2 -0
  68. data/lib/graphql/scalar_type.rb +2 -0
  69. data/lib/graphql/schema.rb +80 -29
  70. data/lib/graphql/schema/argument.rb +25 -7
  71. data/lib/graphql/schema/build_from_definition.rb +150 -58
  72. data/lib/graphql/schema/default_type_error.rb +2 -0
  73. data/lib/graphql/schema/directive.rb +76 -0
  74. data/lib/graphql/schema/directive/flagged.rb +57 -0
  75. data/lib/graphql/schema/enum.rb +3 -0
  76. data/lib/graphql/schema/enum_value.rb +12 -6
  77. data/lib/graphql/schema/field.rb +52 -23
  78. data/lib/graphql/schema/field/connection_extension.rb +10 -8
  79. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  80. data/lib/graphql/schema/input_object.rb +33 -22
  81. data/lib/graphql/schema/interface.rb +1 -0
  82. data/lib/graphql/schema/member.rb +4 -0
  83. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -0
  84. data/lib/graphql/schema/member/build_type.rb +3 -3
  85. data/lib/graphql/schema/member/has_arguments.rb +67 -50
  86. data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
  87. data/lib/graphql/schema/member/has_directives.rb +98 -0
  88. data/lib/graphql/schema/member/has_fields.rb +2 -2
  89. data/lib/graphql/schema/member/has_validators.rb +31 -0
  90. data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
  91. data/lib/graphql/schema/middleware_chain.rb +1 -1
  92. data/lib/graphql/schema/object.rb +11 -0
  93. data/lib/graphql/schema/printer.rb +5 -4
  94. data/lib/graphql/schema/relay_classic_mutation.rb +1 -1
  95. data/lib/graphql/schema/resolver.rb +7 -0
  96. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -0
  97. data/lib/graphql/schema/subscription.rb +19 -1
  98. data/lib/graphql/schema/timeout_middleware.rb +3 -1
  99. data/lib/graphql/schema/unique_within_type.rb +1 -2
  100. data/lib/graphql/schema/validation.rb +4 -2
  101. data/lib/graphql/schema/validator.rb +163 -0
  102. data/lib/graphql/schema/validator/exclusion_validator.rb +31 -0
  103. data/lib/graphql/schema/validator/format_validator.rb +49 -0
  104. data/lib/graphql/schema/validator/inclusion_validator.rb +33 -0
  105. data/lib/graphql/schema/validator/length_validator.rb +57 -0
  106. data/lib/graphql/schema/validator/numericality_validator.rb +71 -0
  107. data/lib/graphql/schema/validator/required_validator.rb +68 -0
  108. data/lib/graphql/static_validation.rb +1 -0
  109. data/lib/graphql/static_validation/all_rules.rb +1 -0
  110. data/lib/graphql/static_validation/rules/fields_will_merge.rb +25 -17
  111. data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
  112. data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
  113. data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
  114. data/lib/graphql/static_validation/validator.rb +32 -9
  115. data/lib/graphql/subscriptions.rb +17 -20
  116. data/lib/graphql/subscriptions/subscription_root.rb +1 -1
  117. data/lib/graphql/tracing.rb +2 -2
  118. data/lib/graphql/tracing/appoptics_tracing.rb +3 -1
  119. data/lib/graphql/tracing/platform_tracing.rb +4 -2
  120. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
  121. data/lib/graphql/tracing/skylight_tracing.rb +1 -1
  122. data/lib/graphql/types/int.rb +9 -2
  123. data/lib/graphql/types/relay.rb +11 -3
  124. data/lib/graphql/types/relay/base_connection.rb +2 -91
  125. data/lib/graphql/types/relay/base_edge.rb +2 -34
  126. data/lib/graphql/types/relay/connection_behaviors.rb +123 -0
  127. data/lib/graphql/types/relay/default_relay.rb +27 -0
  128. data/lib/graphql/types/relay/edge_behaviors.rb +42 -0
  129. data/lib/graphql/types/relay/has_node_field.rb +41 -0
  130. data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
  131. data/lib/graphql/types/relay/node.rb +2 -4
  132. data/lib/graphql/types/relay/node_behaviors.rb +15 -0
  133. data/lib/graphql/types/relay/node_field.rb +1 -19
  134. data/lib/graphql/types/relay/nodes_field.rb +1 -19
  135. data/lib/graphql/types/relay/page_info.rb +2 -14
  136. data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
  137. data/lib/graphql/types/string.rb +7 -1
  138. data/lib/graphql/unauthorized_error.rb +1 -1
  139. data/lib/graphql/union_type.rb +2 -0
  140. data/lib/graphql/upgrader/member.rb +1 -0
  141. data/lib/graphql/upgrader/schema.rb +1 -0
  142. data/lib/graphql/version.rb +1 -1
  143. data/readme.md +1 -1
  144. metadata +50 -6
  145. data/lib/graphql/types/relay/base_field.rb +0 -22
  146. data/lib/graphql/types/relay/base_interface.rb +0 -29
  147. data/lib/graphql/types/relay/base_object.rb +0 -26
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Dataloader
5
+ class Source
6
+ # @api private
7
+ attr_reader :results
8
+
9
+ # Called by {Dataloader} to prepare the {Source}'s internal state
10
+ # @api private
11
+ def setup(dataloader)
12
+ @pending_keys = []
13
+ @results = {}
14
+ @dataloader = dataloader
15
+ end
16
+
17
+ attr_reader :dataloader
18
+
19
+ # @return [Dataloader::Request] a pending request for a value from `key`. Call `.load` on that object to wait for the result.
20
+ def request(key)
21
+ if !@results.key?(key)
22
+ @pending_keys << key
23
+ end
24
+ Dataloader::Request.new(self, key)
25
+ end
26
+
27
+ # @return [Dataloader::Request] a pending request for a values from `keys`. Call `.load` on that object to wait for the results.
28
+ def request_all(keys)
29
+ pending_keys = keys.select { |k| !@results.key?(k) }
30
+ @pending_keys.concat(pending_keys)
31
+ Dataloader::RequestAll.new(self, keys)
32
+ end
33
+
34
+ # @param key [Object] A loading key which will be passed to {#fetch} if it isn't already in the internal cache.
35
+ # @return [Object] The result from {#fetch} for `key`. If `key` hasn't been loaded yet, the Fiber will yield until it's loaded.
36
+ def load(key)
37
+ if @results.key?(key)
38
+ @results[key]
39
+ else
40
+ @pending_keys << key
41
+ sync
42
+ @results[key]
43
+ end
44
+ end
45
+
46
+ # @param keys [Array<Object>] Loading keys which will be passed to `#fetch` (or read from the internal cache).
47
+ # @return [Object] The result from {#fetch} for `keys`. If `keys` haven't been loaded yet, the Fiber will yield until they're loaded.
48
+ def load_all(keys)
49
+ if keys.any? { |k| !@results.key?(k) }
50
+ pending_keys = keys.select { |k| !@results.key?(k) }
51
+ @pending_keys.concat(pending_keys)
52
+ sync
53
+ end
54
+
55
+ keys.map { |k| @results[k] }
56
+ end
57
+
58
+ # Subclasses must implement this method to return a value for each of `keys`
59
+ # @param keys [Array<Object>] keys passed to {#load}, {#load_all}, {#request}, or {#request_all}
60
+ # @return [Array<Object>] A loaded value for each of `keys`. The array must match one-for-one to the list of `keys`.
61
+ def fetch(keys)
62
+ # somehow retrieve these from the backend
63
+ raise "Implement `#{self.class}#fetch(#{keys.inspect}) to return a record for each of the keys"
64
+ end
65
+
66
+ # Wait for a batch, if there's anything to batch.
67
+ # Then run the batch and update the cache.
68
+ # @return [void]
69
+ def sync
70
+ @dataloader.yield
71
+ end
72
+
73
+ # @return [Boolean] True if this source has any pending requests for data.
74
+ def pending?
75
+ @pending_keys.any?
76
+ end
77
+
78
+ # Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
79
+ # @api private
80
+ # @return [void]
81
+ def run_pending_keys
82
+ return if @pending_keys.empty?
83
+ fetch_keys = @pending_keys.uniq
84
+ @pending_keys = []
85
+ results = fetch(fetch_keys)
86
+ fetch_keys.each_with_index do |key, idx|
87
+ @results[key] = results[idx]
88
+ end
89
+ nil
90
+ end
91
+ end
92
+ end
93
+ end
@@ -2,9 +2,9 @@
2
2
  module GraphQL
3
3
  module Define
4
4
  module AssignGlobalIdField
5
- def self.call(type_defn, field_name)
5
+ def self.call(type_defn, field_name, **field_kwargs)
6
6
  resolve = GraphQL::Relay::GlobalIdResolve.new(type: type_defn)
7
- GraphQL::Define::AssignObjectField.call(type_defn, field_name, type: GraphQL::ID_TYPE.to_non_null_type, resolve: resolve)
7
+ GraphQL::Define::AssignObjectField.call(type_defn, field_name, **field_kwargs, type: GraphQL::Deprecation_ID_TYPE.to_non_null_type, resolve: resolve)
8
8
  end
9
9
  end
10
10
  end
@@ -3,6 +3,23 @@ module GraphQL
3
3
  module Define
4
4
  # @api deprecated
5
5
  module InstanceDefinable
6
+ module DeprecatedDefine
7
+ def define(**kwargs, &block)
8
+ deprecated_caller = caller(1, 1).first
9
+ if deprecated_caller.include?("lib/graphql")
10
+ deprecated_caller = caller(2, 10).find { |c| !c.include?("lib/graphql") }
11
+ end
12
+
13
+ if deprecated_caller
14
+ GraphQL::Deprecation.warn <<-ERR
15
+ #{self}.define will be removed in GraphQL-Ruby 2.0; use a class-based definition instead. See https://graphql-ruby.org/schema/class_based_api.html.
16
+ -> called from #{deprecated_caller}
17
+ ERR
18
+ end
19
+ deprecated_define(**kwargs, &block)
20
+ end
21
+ end
22
+
6
23
  def self.included(base)
7
24
  base.extend(ClassMethods)
8
25
  base.ensure_defined(:metadata)
@@ -14,7 +31,7 @@ module GraphQL
14
31
  end
15
32
 
16
33
  # @api deprecated
17
- def define(**kwargs, &block)
34
+ def deprecated_define(**kwargs, &block)
18
35
  # make sure the previous definition_proc was executed:
19
36
  ensure_defined
20
37
  stash_dependent_methods
@@ -22,11 +39,16 @@ module GraphQL
22
39
  nil
23
40
  end
24
41
 
42
+ # @api deprecated
43
+ def define(**kwargs, &block)
44
+ deprecated_define(**kwargs, &block)
45
+ end
46
+
25
47
  # @api deprecated
26
48
  def redefine(**kwargs, &block)
27
49
  ensure_defined
28
50
  new_inst = self.dup
29
- new_inst.define(**kwargs, &block)
51
+ new_inst.deprecated_define(**kwargs, &block)
30
52
  new_inst
31
53
  end
32
54
 
@@ -125,8 +147,16 @@ module GraphQL
125
147
  module ClassMethods
126
148
  # Create a new instance
127
149
  # and prepare a definition using its {.definitions}.
150
+ # @api deprecated
128
151
  # @param kwargs [Hash] Key-value pairs corresponding to defininitions from `accepts_definitions`
129
152
  # @param block [Proc] Block which calls helper methods from `accepts_definitions`
153
+ def deprecated_define(**kwargs, &block)
154
+ instance = self.new
155
+ instance.deprecated_define(**kwargs, &block)
156
+ instance
157
+ end
158
+
159
+ # @api deprecated
130
160
  def define(**kwargs, &block)
131
161
  instance = self.new
132
162
  instance.define(**kwargs, &block)
@@ -7,11 +7,11 @@ module GraphQL
7
7
  class TypeDefiner
8
8
  include Singleton
9
9
  # rubocop:disable Naming/MethodName
10
- def Int; GraphQL::INT_TYPE; end
11
- def String; GraphQL::STRING_TYPE; end
12
- def Float; GraphQL::FLOAT_TYPE; end
13
- def Boolean; GraphQL::BOOLEAN_TYPE; end
14
- def ID; GraphQL::ID_TYPE; end
10
+ def Int; GraphQL::Deprecation_INT_TYPE; end
11
+ def String; GraphQL::Deprecation_STRING_TYPE; end
12
+ def Float; GraphQL::Deprecation_FLOAT_TYPE; end
13
+ def Boolean; GraphQL::Deprecation_BOOLEAN_TYPE; end
14
+ def ID; GraphQL::Deprecation_ID_TYPE; end
15
15
  # rubocop:enable Naming/MethodName
16
16
 
17
17
  # Make a {ListType} which wraps the input type
@@ -4,13 +4,13 @@ module GraphQL
4
4
  #
5
5
  # 1. Scoped by file (CRuby only), add to the top of the file:
6
6
  #
7
- # using GraphQL::DeprecatedDSL
7
+ # using GraphQL::DeprecationDSL
8
8
  #
9
9
  # (This is a "refinement", there are also other ways to scope it.)
10
10
  #
11
11
  # 2. Global application, add before schema definition:
12
12
  #
13
- # GraphQL::DeprecatedDSL.activate
13
+ # GraphQL::DeprecationDSL.activate
14
14
  #
15
15
  module DeprecatedDSL
16
16
  TYPE_CLASSES = [
@@ -23,12 +23,17 @@ module GraphQL
23
23
  ]
24
24
 
25
25
  def self.activate
26
+ deprecated_caller = caller(1, 1).first
27
+ GraphQL::Deprecation.warn "DeprecatedDSL will be removed from GraphQL-Ruby 2.0, use `.to_non_null_type` instead of `!` and remove `.activate` from #{deprecated_caller}"
26
28
  TYPE_CLASSES.each { |c| c.extend(Methods) }
27
29
  GraphQL::Schema::List.include(Methods)
28
30
  GraphQL::Schema::NonNull.include(Methods)
29
31
  end
32
+
30
33
  module Methods
31
34
  def !
35
+ deprecated_caller = caller(1, 1).first
36
+ GraphQL::Deprecation.warn "DeprecatedDSL will be removed from GraphQL-Ruby 2.0, use `.to_non_null_type` instead of `!` at #{deprecated_caller}"
32
37
  to_non_null_type
33
38
  end
34
39
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Deprecation
5
+ def self.warn(message)
6
+ if defined?(ActiveSupport::Deprecation)
7
+ ActiveSupport::Deprecation.warn(message)
8
+ else
9
+ Kernel.warn(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,6 +2,8 @@
2
2
  module GraphQL
3
3
  # @api deprecated
4
4
  class EnumType < GraphQL::BaseType
5
+ extend Define::InstanceDefinable::DeprecatedDefine
6
+
5
7
  accepts_definitions :values, value: GraphQL::Define::AssignEnumValue
6
8
  ensure_defined(:values, :validate_non_null_input, :coerce_non_null_input, :coerce_result)
7
9
  attr_accessor :ast_node
@@ -18,6 +18,10 @@ module GraphQL
18
18
  #
19
19
  class Errors
20
20
  def self.use(schema)
21
+ if schema.plugins.any? { |(plugin, kwargs)| plugin == self }
22
+ definition_line = caller(2, 1).first
23
+ GraphQL::Deprecation.warn("GraphQL::Execution::Errors is now installed by default, remove `use GraphQL::Execution::Errors` from #{definition_line}")
24
+ end
21
25
  schema.error_handler = self.new(schema)
22
26
  end
23
27
 
@@ -18,7 +18,14 @@ module GraphQL
18
18
  # @api private
19
19
  PROPAGATE_NULL = PropagateNull.new
20
20
 
21
+ def self.use(schema_class)
22
+ schema_class.query_execution_strategy(self)
23
+ schema_class.mutation_execution_strategy(self)
24
+ schema_class.subscription_execution_strategy(self)
25
+ end
26
+
21
27
  def execute(ast_operation, root_type, query)
28
+ GraphQL::Deprecation.warn "#{self.class} will be removed in GraphQL-Ruby 2.0, please upgrade to the Interpreter: https://graphql-ruby.org/queries/interpreter.html"
22
29
  result = resolve_root_selection(query)
23
30
  lazy_resolve_root_selection(result, **{query: query})
24
31
  GraphQL::Execution::Flatten.call(query.context)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "fiber"
2
3
  require "graphql/execution/interpreter/argument_value"
3
4
  require "graphql/execution/interpreter/arguments"
4
5
  require "graphql/execution/interpreter/arguments_cache"
@@ -22,12 +23,15 @@ module GraphQL
22
23
  end
23
24
 
24
25
  def self.use(schema_class)
25
- schema_class.interpreter = true
26
- schema_class.query_execution_strategy(GraphQL::Execution::Interpreter)
27
- schema_class.mutation_execution_strategy(GraphQL::Execution::Interpreter)
28
- schema_class.subscription_execution_strategy(GraphQL::Execution::Interpreter)
29
- schema_class.add_subscription_extension_if_necessary
30
- GraphQL::Schema::Object.include(HandlesRawValue)
26
+ if schema_class.interpreter?
27
+ definition_line = caller(2, 1).first
28
+ GraphQL::Deprecation.warn("GraphQL::Execution::Interpreter is now the default; remove `use GraphQL::Execution::Interpreter` from the schema definition (#{definition_line})")
29
+ else
30
+ schema_class.query_execution_strategy(self)
31
+ schema_class.mutation_execution_strategy(self)
32
+ schema_class.subscription_execution_strategy(self)
33
+ schema_class.add_subscription_extension_if_necessary
34
+ end
31
35
  end
32
36
 
33
37
  def self.begin_multiplex(multiplex)
@@ -5,6 +5,9 @@ module GraphQL
5
5
  class Interpreter
6
6
  # A wrapper for argument hashes in GraphQL queries.
7
7
  #
8
+ # This object is immutable so that the runtime code can be sure that
9
+ # modifications don't leak from one use to another
10
+ #
8
11
  # @see GraphQL::Query#arguments_for to get access to these objects.
9
12
  class Arguments
10
13
  extend Forwardable
@@ -16,20 +19,69 @@ module GraphQL
16
19
  # @return [Hash<Symbol, Object>]
17
20
  attr_reader :keyword_arguments
18
21
 
19
- def initialize(keyword_arguments:, argument_values:)
20
- @keyword_arguments = keyword_arguments
21
- @argument_values = argument_values
22
+ # @param argument_values [nil, Hash{Symbol => ArgumentValue}]
23
+ # @param keyword_arguments [nil, Hash{Symbol => Object}]
24
+ def initialize(keyword_arguments: nil, argument_values:)
25
+ @empty = argument_values.nil? || argument_values.empty?
26
+ # This is only present when `extras` have been merged in:
27
+ if keyword_arguments
28
+ # This is a little crazy. We expect the `:argument_details` extra to _include extras_,
29
+ # but the object isn't created until _after_ extras are put together.
30
+ # So, we have to use a special flag here to say, "at the last minute, add yourself to the keyword args."
31
+ #
32
+ # Otherwise:
33
+ # - We can't access the final Arguments instance _while_ we're preparing extras
34
+ # - After we _can_ access it, it's frozen, so we can't add anything.
35
+ #
36
+ # So, this flag gives us a chance to sneak it in before freezing, _and_ while we have access
37
+ # to the new Arguments instance itself.
38
+ if keyword_arguments[:argument_details] == :__arguments_add_self
39
+ keyword_arguments[:argument_details] = self
40
+ end
41
+ @keyword_arguments = keyword_arguments.freeze
42
+ elsif !@empty
43
+ @keyword_arguments = {}
44
+ argument_values.each do |name, arg_val|
45
+ @keyword_arguments[name] = arg_val.value
46
+ end
47
+ @keyword_arguments.freeze
48
+ else
49
+ @keyword_arguments = NO_ARGS
50
+ end
51
+ @argument_values = argument_values ? argument_values.freeze : NO_ARGS
52
+ freeze
22
53
  end
23
54
 
24
55
  # @return [Hash{Symbol => ArgumentValue}]
25
56
  attr_reader :argument_values
26
57
 
27
- def_delegators :@keyword_arguments, :key?, :[], :fetch, :keys, :each, :values
28
- def_delegators :@argument_values, :each_value
58
+ def empty?
59
+ @empty
60
+ end
61
+
62
+ def_delegators :keyword_arguments, :key?, :[], :fetch, :keys, :each, :values
63
+ def_delegators :argument_values, :each_value
29
64
 
30
65
  def inspect
31
66
  "#<#{self.class} @keyword_arguments=#{keyword_arguments.inspect}>"
32
67
  end
68
+
69
+ # Create a new arguments instance which includes these extras.
70
+ #
71
+ # This is called by the runtime to implement field `extras: [...]`
72
+ #
73
+ # @param extra_args [Hash<Symbol => Object>]
74
+ # @return [Interpreter::Arguments]
75
+ # @api private
76
+ def merge_extras(extra_args)
77
+ self.class.new(
78
+ argument_values: argument_values,
79
+ keyword_arguments: keyword_arguments.merge(extra_args)
80
+ )
81
+ end
82
+
83
+ NO_ARGS = {}.freeze
84
+ EMPTY = self.new(argument_values: nil, keyword_arguments: NO_ARGS).freeze
33
85
  end
34
86
  end
35
87
  end
@@ -29,11 +29,16 @@ module GraphQL
29
29
 
30
30
  private
31
31
 
32
+ NO_ARGUMENTS = {}.freeze
33
+
32
34
  NO_VALUE_GIVEN = Object.new
33
35
 
34
36
  def prepare_args_hash(ast_arg_or_hash_or_value)
35
37
  case ast_arg_or_hash_or_value
36
38
  when Hash
39
+ if ast_arg_or_hash_or_value.empty?
40
+ return NO_ARGUMENTS
41
+ end
37
42
  args_hash = {}
38
43
  ast_arg_or_hash_or_value.each do |k, v|
39
44
  args_hash[k] = prepare_args_hash(v)
@@ -42,6 +47,9 @@ module GraphQL
42
47
  when Array
43
48
  ast_arg_or_hash_or_value.map { |v| prepare_args_hash(v) }
44
49
  when GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::InputObject, GraphQL::Language::Nodes::Directive
50
+ if ast_arg_or_hash_or_value.arguments.empty?
51
+ return NO_ARGUMENTS
52
+ end
45
53
  args_hash = {}
46
54
  ast_arg_or_hash_or_value.arguments.each do |arg|
47
55
  v = prepare_args_hash(arg.value)
@@ -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
@@ -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 = {}
@@ -47,15 +49,24 @@ module GraphQL
47
49
  root_op_type = root_operation.operation_type || "query"
48
50
  root_type = schema.root_type_for_operation(root_op_type)
49
51
  path = []
50
- set_interpreter_context(:current_object, query.root_value)
51
- set_interpreter_context(:current_path, path)
52
+ set_all_interpreter_context(query.root_value, nil, nil, path)
52
53
  object_proxy = authorized_new(root_type, query.root_value, context, path)
53
54
  object_proxy = schema.sync_lazy(object_proxy)
54
55
  if object_proxy.nil?
55
56
  # Root .authorized? returned false.
56
57
  write_in_response(path, nil)
57
58
  else
58
- evaluate_selections(path, context.scoped_context, object_proxy, root_type, root_operation.selections, root_operation_type: root_op_type)
59
+ # Prepare this runtime state to be encapsulated in a Fiber
60
+ @progress_path = path
61
+ @progress_scoped_context = context.scoped_context
62
+ @progress_object = object_proxy
63
+ @progress_object_type = root_type
64
+ @progress_index = nil
65
+ @progress_is_eager_selection = root_op_type == "mutation"
66
+ @progress_selections = gather_selections(object_proxy, root_type, root_operation.selections)
67
+
68
+ # Make the first fiber which will begin execution
69
+ enqueue_selections_fiber
59
70
  end
60
71
  delete_interpreter_context(:current_path)
61
72
  delete_interpreter_context(:current_field)
@@ -64,7 +75,33 @@ module GraphQL
64
75
  nil
65
76
  end
66
77
 
67
- def gather_selections(owner_object, owner_type, selections, selections_by_name)
78
+ # Use `@dataloader` to enqueue a fiber that will pick up from the current point.
79
+ # @return [void]
80
+ def enqueue_selections_fiber
81
+ # Read these into local variables so that later assignments don't affect the block below.
82
+ path = @progress_path
83
+ scoped_context = @progress_scoped_context
84
+ owner_object = @progress_object
85
+ owner_type = @progress_object_type
86
+ idx = @progress_index
87
+ is_eager_selection = @progress_is_eager_selection
88
+ gathered_selections = @progress_selections
89
+
90
+ @dataloader.enqueue {
91
+ evaluate_selections(
92
+ path,
93
+ scoped_context,
94
+ owner_object,
95
+ owner_type,
96
+ is_eager_selection: is_eager_selection,
97
+ after: idx,
98
+ gathered_selections: gathered_selections,
99
+ )
100
+ }
101
+ nil
102
+ end
103
+
104
+ def gather_selections(owner_object, owner_type, selections, selections_by_name = {})
68
105
  selections.each do |node|
69
106
  # Skip gathering this if the directive says so
70
107
  if !directives_include?(node, owner_object, owner_type)
@@ -116,139 +153,186 @@ module GraphQL
116
153
  raise "Invariant: unexpected selection class: #{node.class}"
117
154
  end
118
155
  end
156
+ selections_by_name
119
157
  end
120
158
 
121
- def evaluate_selections(path, scoped_context, owner_object, owner_type, selections, root_operation_type: nil)
122
- set_interpreter_context(:current_object, owner_object)
123
- set_interpreter_context(:current_path, 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
133
- else
134
- field_ast_nodes = nil
135
- ast_node = field_ast_nodes_or_ast_node
159
+ NO_ARGS = {}.freeze
160
+
161
+ # @return [void]
162
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection:, gathered_selections:, after:)
163
+ set_all_interpreter_context(owner_object, nil, nil, path)
164
+
165
+ @progress_path = path
166
+ @progress_scoped_context = scoped_context
167
+ @progress_object = owner_object
168
+ @progress_object_type = owner_type
169
+ @progress_index = nil
170
+ @progress_is_eager_selection = is_eager_selection
171
+ @progress_selections = gathered_selections
172
+
173
+ # Track `idx` manually to avoid an allocation on this hot path
174
+ idx = 0
175
+ gathered_selections.each do |result_name, field_ast_nodes_or_ast_node|
176
+ prev_idx = idx
177
+ idx += 1
178
+ # TODO: this is how a `progress` resumes where this left off.
179
+ # Is there a better way to seek in the hash?
180
+ # I think we could also use the array of keys; it supports seeking just fine.
181
+ if after && prev_idx <= after
182
+ next
136
183
  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
184
+ @progress_index = prev_idx
185
+ # This is how the current runtime gives itself to `dataloader`
186
+ # so that the dataloader can enqueue another fiber to resume if needed.
187
+ @dataloader.current_runtime = self
188
+ evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_selection)
189
+ # The dataloader knows if ^^ that selection halted and later selections were executed in another fiber.
190
+ # If that's the case, then don't continue execution here.
191
+ if @dataloader.yielded?(path)
192
+ break
150
193
  end
151
- return_type = field_defn.type
194
+ end
195
+ nil
196
+ end
152
197
 
153
- next_path = path.dup
154
- next_path << result_name
155
- next_path.freeze
198
+ attr_reader :progress_path
156
199
 
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_interpreter_context(:current_path, next_path)
163
- set_interpreter_context(:current_field, field_defn)
200
+ # @return [void]
201
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
202
+ # As a performance optimization, the hash key will be a `Node` if
203
+ # there's only one selection of the field. But if there are multiple
204
+ # selections of the field, it will be an Array of nodes
205
+ if field_ast_nodes_or_ast_node.is_a?(Array)
206
+ field_ast_nodes = field_ast_nodes_or_ast_node
207
+ ast_node = field_ast_nodes.first
208
+ else
209
+ field_ast_nodes = nil
210
+ ast_node = field_ast_nodes_or_ast_node
211
+ end
212
+ field_name = ast_node.name
213
+ field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
214
+ is_introspection = false
215
+ if field_defn.nil?
216
+ field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
217
+ is_introspection = true
218
+ entry_point_field
219
+ elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
220
+ is_introspection = true
221
+ dynamic_field
222
+ else
223
+ raise "Invariant: no field for #{owner_type}.#{field_name}"
224
+ end
225
+ end
226
+ return_type = field_defn.type
164
227
 
165
- context.scoped_context = scoped_context
166
- object = owner_object
228
+ next_path = path.dup
229
+ next_path << result_name
230
+ next_path.freeze
167
231
 
168
- if is_introspection
169
- object = authorized_new(field_defn.owner, object, context, next_path)
170
- end
232
+ # This seems janky, but we need to know
233
+ # the field's return type at this path in order
234
+ # to propagate `null`
235
+ set_type_at_path(next_path, return_type)
236
+ # Set this before calling `run_with_directives`, so that the directive can have the latest path
237
+ set_all_interpreter_context(nil, field_defn, nil, next_path)
171
238
 
172
- begin
173
- kwarg_arguments = arguments(object, field_defn, ast_node)
174
- rescue GraphQL::ExecutionError => e
175
- continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
176
- next
177
- end
239
+ context.scoped_context = scoped_context
240
+ object = owner_object
178
241
 
179
- 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|
180
- if resolved_arguments.is_a? GraphQL::ExecutionError
181
- continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
182
- next
183
- end
242
+ if is_introspection
243
+ object = authorized_new(field_defn.owner, object, context, next_path)
244
+ end
184
245
 
185
- kwarg_arguments = resolved_arguments.keyword_arguments
246
+ begin
247
+ kwarg_arguments = arguments(object, field_defn, ast_node)
248
+ rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => e
249
+ continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
250
+ return
251
+ end
186
252
 
253
+ 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|
254
+ if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
255
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
256
+ next
257
+ end
258
+
259
+ kwarg_arguments = if resolved_arguments.empty? && field_defn.extras.empty?
260
+ # We can avoid allocating the `{ Symbol => Object }` hash in this case
261
+ NO_ARGS
262
+ else
263
+ # Bundle up the extras, then make a new arguments instance
264
+ # that includes the extras, too.
265
+ extra_args = {}
187
266
  field_defn.extras.each do |extra|
188
267
  case extra
189
268
  when :ast_node
190
- kwarg_arguments[:ast_node] = ast_node
269
+ extra_args[:ast_node] = ast_node
191
270
  when :execution_errors
192
- kwarg_arguments[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
271
+ extra_args[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
193
272
  when :path
194
- kwarg_arguments[:path] = next_path
273
+ extra_args[:path] = next_path
195
274
  when :lookahead
196
275
  if !field_ast_nodes
197
276
  field_ast_nodes = [ast_node]
198
277
  end
199
- kwarg_arguments[:lookahead] = Execution::Lookahead.new(
278
+
279
+ extra_args[:lookahead] = Execution::Lookahead.new(
200
280
  query: query,
201
281
  ast_nodes: field_ast_nodes,
202
282
  field: field_defn,
203
283
  )
204
284
  when :argument_details
205
- kwarg_arguments[:argument_details] = resolved_arguments
285
+ # Use this flag to tell Interpreter::Arguments to add itself
286
+ # to the keyword args hash _before_ freezing everything.
287
+ extra_args[:argument_details] = :__arguments_add_self
206
288
  else
207
- kwarg_arguments[extra] = field_defn.fetch_extra(extra, context)
289
+ extra_args[extra] = field_defn.fetch_extra(extra, context)
208
290
  end
209
291
  end
292
+ resolved_arguments = resolved_arguments.merge_extras(extra_args)
293
+ resolved_arguments.keyword_arguments
294
+ end
210
295
 
211
- set_interpreter_context(:current_arguments, kwarg_arguments)
296
+ set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
212
297
 
213
- # Optimize for the case that field is selected only once
214
- if field_ast_nodes.nil? || field_ast_nodes.size == 1
215
- next_selections = ast_node.selections
216
- else
217
- next_selections = []
218
- field_ast_nodes.each { |f| next_selections.concat(f.selections) }
219
- end
298
+ # Optimize for the case that field is selected only once
299
+ if field_ast_nodes.nil? || field_ast_nodes.size == 1
300
+ next_selections = ast_node.selections
301
+ else
302
+ next_selections = []
303
+ field_ast_nodes.each { |f| next_selections.concat(f.selections) }
304
+ end
220
305
 
221
- field_result = resolve_with_directives(object, ast_node) do
222
- # Actually call the field resolver and capture the result
223
- app_result = begin
224
- query.with_error_handling do
225
- query.trace("execute_field", {owner: owner_type, field: field_defn, path: next_path, query: query, object: object, arguments: kwarg_arguments}) do
226
- field_defn.resolve(object, kwarg_arguments, context)
227
- end
306
+ field_result = resolve_with_directives(object, ast_node) do
307
+ # Actually call the field resolver and capture the result
308
+ app_result = begin
309
+ query.with_error_handling do
310
+ 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
311
+ field_defn.resolve(object, kwarg_arguments, context)
228
312
  end
229
- rescue GraphQL::ExecutionError => err
230
- err
231
313
  end
232
- 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|
233
- continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
234
- if RawValue === continue_value
235
- # Write raw value directly to the response without resolving nested objects
236
- write_in_response(next_path, continue_value.resolve)
237
- elsif HALT != continue_value
238
- continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
239
- end
314
+ rescue GraphQL::ExecutionError => err
315
+ err
316
+ end
317
+ 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|
318
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
319
+ if RawValue === continue_value
320
+ # Write raw value directly to the response without resolving nested objects
321
+ write_in_response(next_path, continue_value.resolve)
322
+ elsif HALT != continue_value
323
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
240
324
  end
241
325
  end
326
+ end
242
327
 
243
- # If this field is a root mutation field, immediately resolve
244
- # all of its child fields before moving on to the next root mutation field.
245
- # (Subselections of this mutation will still be resolved level-by-level.)
246
- if root_operation_type == "mutation"
247
- Interpreter::Resolve.resolve_all([field_result])
248
- else
249
- field_result
250
- end
328
+ # If this field is a root mutation field, immediately resolve
329
+ # all of its child fields before moving on to the next root mutation field.
330
+ # (Subselections of this mutation will still be resolved level-by-level.)
331
+ if is_eager_field
332
+ Interpreter::Resolve.resolve_all([field_result])
251
333
  end
334
+
335
+ nil
252
336
  end
253
337
  end
254
338
 
@@ -309,7 +393,7 @@ module GraphQL
309
393
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
310
394
  resolved_value ||= value
311
395
 
312
- 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|
396
+ 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|
313
397
  possible_types = query.possible_types(current_type)
314
398
 
315
399
  if !possible_types.include?(resolved_type)
@@ -329,12 +413,13 @@ module GraphQL
329
413
  rescue GraphQL::ExecutionError => err
330
414
  err
331
415
  end
332
- 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|
416
+ 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|
333
417
  continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
334
418
  if HALT != continue_value
335
419
  response_hash = {}
336
420
  write_in_response(path, response_hash)
337
- evaluate_selections(path, context.scoped_context, continue_value, current_type, next_selections)
421
+ gathered_selections = gather_selections(continue_value, current_type, next_selections)
422
+ evaluate_selections(path, context.scoped_context, continue_value, current_type, is_eager_selection: false, gathered_selections: gathered_selections, after: nil)
338
423
  response_hash
339
424
  end
340
425
  end
@@ -352,7 +437,7 @@ module GraphQL
352
437
  idx += 1
353
438
  set_type_at_path(next_path, inner_type)
354
439
  # This will update `response_list` with the lazy
355
- 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|
440
+ 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|
356
441
  continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
357
442
  if HALT != continue_value
358
443
  continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
@@ -414,30 +499,39 @@ module GraphQL
414
499
  true
415
500
  end
416
501
 
502
+ def set_all_interpreter_context(object, field, arguments, path)
503
+ if object
504
+ @context[:current_object] = @interpreter_context[:current_object] = object
505
+ end
506
+ if field
507
+ @context[:current_field] = @interpreter_context[:current_field] = field
508
+ end
509
+ if arguments
510
+ @context[:current_arguments] = @interpreter_context[:current_arguments] = arguments
511
+ end
512
+ if path
513
+ @context[:current_path] = @interpreter_context[:current_path] = path
514
+ end
515
+ end
516
+
417
517
  # @param obj [Object] Some user-returned value that may want to be batched
418
518
  # @param path [Array<String>]
419
519
  # @param field [GraphQL::Schema::Field]
420
520
  # @param eager [Boolean] Set to `true` for mutation root fields only
421
521
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
422
522
  # @return [GraphQL::Execution::Lazy, Object] If loading `object` will be deferred, it's a wrapper over it.
423
- def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, eager: false, trace: true, &block)
424
- set_interpreter_context(:current_object, owner_object)
425
- set_interpreter_context(:current_arguments, arguments)
426
- set_interpreter_context(:current_path, path)
427
- set_interpreter_context(:current_field, field)
523
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
524
+ set_all_interpreter_context(owner_object, field, arguments, path)
428
525
  if schema.lazy?(lazy_obj)
429
526
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
430
- set_interpreter_context(:current_path, path)
431
- set_interpreter_context(:current_field, field)
432
- set_interpreter_context(:current_object, owner_object)
433
- set_interpreter_context(:current_arguments, arguments)
527
+ set_all_interpreter_context(owner_object, field, arguments, path)
434
528
  context.scoped_context = scoped_context
435
529
  # Wrap the execution of _this_ method with tracing,
436
530
  # but don't wrap the continuation below
437
531
  inner_obj = begin
438
532
  query.with_error_handling do
439
533
  if trace
440
- query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments}) do
534
+ query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
441
535
  schema.sync_lazy(lazy_obj)
442
536
  end
443
537
  else
@@ -447,7 +541,7 @@ module GraphQL
447
541
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
448
542
  err
449
543
  end
450
- 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)
544
+ 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)
451
545
  end
452
546
 
453
547
  if eager
@@ -462,9 +556,7 @@ module GraphQL
462
556
  end
463
557
 
464
558
  def arguments(graphql_object, arg_owner, ast_node)
465
- # Don't cache arguments if field extras or extensions are requested since they can mutate the argument data structure
466
- if arg_owner.arguments_statically_coercible? &&
467
- (!arg_owner.is_a?(GraphQL::Schema::Field) || (arg_owner.extras.empty? && arg_owner.extensions.empty?))
559
+ if arg_owner.arguments_statically_coercible?
468
560
  query.arguments_for(ast_node, arg_owner)
469
561
  else
470
562
  # The arguments must be prepared in the context of the given object
@@ -505,6 +597,16 @@ module GraphQL
505
597
  end
506
598
  end
507
599
 
600
+ def value_at(path)
601
+ i = 0
602
+ value = @response.final_value
603
+ while value && (part = path[i])
604
+ value = value[part]
605
+ i += 1
606
+ end
607
+ value
608
+ end
609
+
508
610
  # To propagate nulls, we have to know what the field type was
509
611
  # at previous parts of the response.
510
612
  # This hash matches the response