graphql 1.11.7 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +5 -5
  3. data/lib/generators/graphql/relay_generator.rb +63 -0
  4. data/lib/generators/graphql/templates/base_connection.erb +8 -0
  5. data/lib/generators/graphql/templates/base_edge.erb +8 -0
  6. data/lib/generators/graphql/templates/node_type.erb +9 -0
  7. data/lib/generators/graphql/templates/object.erb +1 -1
  8. data/lib/generators/graphql/templates/query_type.erb +1 -3
  9. data/lib/generators/graphql/templates/schema.erb +8 -35
  10. data/lib/graphql.rb +38 -4
  11. data/lib/graphql/analysis/analyze_query.rb +7 -0
  12. data/lib/graphql/analysis/ast.rb +11 -2
  13. data/lib/graphql/analysis/ast/visitor.rb +9 -1
  14. data/lib/graphql/backtrace.rb +28 -19
  15. data/lib/graphql/backtrace/legacy_tracer.rb +56 -0
  16. data/lib/graphql/backtrace/table.rb +22 -2
  17. data/lib/graphql/backtrace/tracer.rb +40 -9
  18. data/lib/graphql/backwards_compatibility.rb +1 -0
  19. data/lib/graphql/compatibility/execution_specification.rb +1 -0
  20. data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
  21. data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
  22. data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
  23. data/lib/graphql/dataloader.rb +197 -0
  24. data/lib/graphql/dataloader/null_dataloader.rb +21 -0
  25. data/lib/graphql/dataloader/request.rb +24 -0
  26. data/lib/graphql/dataloader/request_all.rb +22 -0
  27. data/lib/graphql/dataloader/source.rb +93 -0
  28. data/lib/graphql/define/assign_global_id_field.rb +1 -1
  29. data/lib/graphql/define/instance_definable.rb +32 -2
  30. data/lib/graphql/define/type_definer.rb +5 -5
  31. data/lib/graphql/deprecated_dsl.rb +5 -0
  32. data/lib/graphql/enum_type.rb +2 -0
  33. data/lib/graphql/execution/errors.rb +4 -0
  34. data/lib/graphql/execution/execute.rb +7 -0
  35. data/lib/graphql/execution/interpreter.rb +10 -6
  36. data/lib/graphql/execution/interpreter/arguments.rb +51 -14
  37. data/lib/graphql/execution/interpreter/handles_raw_value.rb +0 -7
  38. data/lib/graphql/execution/interpreter/runtime.rb +210 -124
  39. data/lib/graphql/execution/multiplex.rb +20 -6
  40. data/lib/graphql/function.rb +4 -0
  41. data/lib/graphql/input_object_type.rb +2 -0
  42. data/lib/graphql/interface_type.rb +3 -1
  43. data/lib/graphql/language/document_from_schema_definition.rb +50 -23
  44. data/lib/graphql/object_type.rb +2 -0
  45. data/lib/graphql/pagination/connection.rb +5 -1
  46. data/lib/graphql/pagination/connections.rb +6 -16
  47. data/lib/graphql/query.rb +2 -0
  48. data/lib/graphql/query/context.rb +4 -0
  49. data/lib/graphql/query/serial_execution.rb +1 -0
  50. data/lib/graphql/relay/base_connection.rb +7 -0
  51. data/lib/graphql/relay/connection_instrumentation.rb +4 -4
  52. data/lib/graphql/relay/connection_type.rb +1 -1
  53. data/lib/graphql/relay/mutation.rb +1 -0
  54. data/lib/graphql/relay/node.rb +3 -0
  55. data/lib/graphql/relay/type_extensions.rb +2 -0
  56. data/lib/graphql/scalar_type.rb +2 -0
  57. data/lib/graphql/schema.rb +61 -23
  58. data/lib/graphql/schema/argument.rb +25 -7
  59. data/lib/graphql/schema/build_from_definition.rb +139 -51
  60. data/lib/graphql/schema/directive.rb +76 -0
  61. data/lib/graphql/schema/directive/flagged.rb +57 -0
  62. data/lib/graphql/schema/enum.rb +3 -0
  63. data/lib/graphql/schema/enum_value.rb +12 -6
  64. data/lib/graphql/schema/field.rb +28 -9
  65. data/lib/graphql/schema/field/connection_extension.rb +3 -2
  66. data/lib/graphql/schema/input_object.rb +33 -22
  67. data/lib/graphql/schema/interface.rb +1 -0
  68. data/lib/graphql/schema/member.rb +4 -0
  69. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -0
  70. data/lib/graphql/schema/member/build_type.rb +3 -3
  71. data/lib/graphql/schema/member/has_arguments.rb +24 -6
  72. data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
  73. data/lib/graphql/schema/member/has_directives.rb +98 -0
  74. data/lib/graphql/schema/member/has_validators.rb +31 -0
  75. data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
  76. data/lib/graphql/schema/object.rb +11 -0
  77. data/lib/graphql/schema/printer.rb +5 -4
  78. data/lib/graphql/schema/resolver.rb +7 -0
  79. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -0
  80. data/lib/graphql/schema/subscription.rb +19 -1
  81. data/lib/graphql/schema/timeout_middleware.rb +2 -0
  82. data/lib/graphql/schema/validation.rb +2 -0
  83. data/lib/graphql/schema/validator.rb +163 -0
  84. data/lib/graphql/schema/validator/exclusion_validator.rb +31 -0
  85. data/lib/graphql/schema/validator/format_validator.rb +49 -0
  86. data/lib/graphql/schema/validator/inclusion_validator.rb +33 -0
  87. data/lib/graphql/schema/validator/length_validator.rb +57 -0
  88. data/lib/graphql/schema/validator/numericality_validator.rb +71 -0
  89. data/lib/graphql/schema/validator/required_validator.rb +68 -0
  90. data/lib/graphql/static_validation/validator.rb +2 -0
  91. data/lib/graphql/subscriptions.rb +17 -20
  92. data/lib/graphql/tracing.rb +2 -2
  93. data/lib/graphql/tracing/appoptics_tracing.rb +3 -1
  94. data/lib/graphql/tracing/platform_tracing.rb +3 -1
  95. data/lib/graphql/tracing/skylight_tracing.rb +1 -1
  96. data/lib/graphql/types/relay.rb +11 -3
  97. data/lib/graphql/types/relay/base_connection.rb +2 -92
  98. data/lib/graphql/types/relay/base_edge.rb +2 -35
  99. data/lib/graphql/types/relay/connection_behaviors.rb +123 -0
  100. data/lib/graphql/types/relay/default_relay.rb +27 -0
  101. data/lib/graphql/types/relay/edge_behaviors.rb +42 -0
  102. data/lib/graphql/types/relay/has_node_field.rb +41 -0
  103. data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
  104. data/lib/graphql/types/relay/node.rb +2 -4
  105. data/lib/graphql/types/relay/node_behaviors.rb +15 -0
  106. data/lib/graphql/types/relay/node_field.rb +1 -19
  107. data/lib/graphql/types/relay/nodes_field.rb +1 -19
  108. data/lib/graphql/types/relay/page_info.rb +2 -14
  109. data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
  110. data/lib/graphql/union_type.rb +2 -0
  111. data/lib/graphql/upgrader/member.rb +1 -0
  112. data/lib/graphql/upgrader/schema.rb +1 -0
  113. data/lib/graphql/version.rb +1 -1
  114. metadata +30 -5
  115. data/lib/graphql/types/relay/base_field.rb +0 -22
  116. data/lib/graphql/types/relay/base_interface.rb +0 -29
  117. 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
@@ -4,7 +4,7 @@ module GraphQL
4
4
  module AssignGlobalIdField
5
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, **field_kwargs, type: GraphQL::ID_TYPE.to_non_null_type, resolve: resolve)
7
+ GraphQL::Define::AssignObjectField.call(type_defn, field_name, **field_kwargs, type: GraphQL::DEPRECATED_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
+ 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::DEPRECATED_INT_TYPE; end
11
+ def String; GraphQL::DEPRECATED_STRING_TYPE; end
12
+ def Float; GraphQL::DEPRECATED_FLOAT_TYPE; end
13
+ def Boolean; GraphQL::DEPRECATED_BOOLEAN_TYPE; end
14
+ def ID; GraphQL::DEPRECATED_ID_TYPE; end
15
15
  # rubocop:enable Naming/MethodName
16
16
 
17
17
  # Make a {ListType} which wraps the input type
@@ -23,12 +23,17 @@ module GraphQL
23
23
  ]
24
24
 
25
25
  def self.activate
26
+ deprecated_caller = caller(1, 1).first
27
+ 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
+ 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
@@ -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
+ 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
+ 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
+ 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
@@ -14,26 +17,43 @@ module GraphQL
14
17
  # This hash is the one used at runtime.
15
18
  #
16
19
  # @return [Hash<Symbol, Object>]
17
- def keyword_arguments
18
- @keyword_arguments ||= begin
19
- kwargs = {}
20
- argument_values.each do |name, arg_val|
21
- kwargs[name] = arg_val.value
22
- end
23
- kwargs
24
- end
25
- end
20
+ attr_reader :keyword_arguments
26
21
 
27
22
  # @param argument_values [nil, Hash{Symbol => ArgumentValue}]
28
- def initialize(argument_values:)
29
- @argument_values = argument_values
23
+ # @param keyword_arguments [nil, Hash{Symbol => Object}]
24
+ def initialize(keyword_arguments: nil, argument_values:)
30
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
31
53
  end
32
54
 
33
55
  # @return [Hash{Symbol => ArgumentValue}]
34
- def argument_values
35
- @argument_values ||= {}
36
- end
56
+ attr_reader :argument_values
37
57
 
38
58
  def empty?
39
59
  @empty
@@ -45,6 +65,23 @@ module GraphQL
45
65
  def inspect
46
66
  "#<#{self.class} @keyword_arguments=#{keyword_arguments.inspect}>"
47
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
48
85
  end
49
86
  end
50
87
  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
@@ -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,17 @@ 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
+ # 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
58
70
  end
59
71
  delete_interpreter_context(:current_path)
60
72
  delete_interpreter_context(:current_field)
@@ -63,7 +75,33 @@ module GraphQL
63
75
  nil
64
76
  end
65
77
 
66
- 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 = {})
67
105
  selections.each do |node|
68
106
  # Skip gathering this if the directive says so
69
107
  if !directives_include?(node, owner_object, owner_type)
@@ -115,145 +153,184 @@ module GraphQL
115
153
  raise "Invariant: unexpected selection class: #{node.class}"
116
154
  end
117
155
  end
156
+ selections_by_name
118
157
  end
119
158
 
120
159
  NO_ARGS = {}.freeze
121
160
 
122
- def evaluate_selections(path, scoped_context, owner_object, owner_type, selections, root_operation_type: nil)
161
+ # @return [void]
162
+ def evaluate_selections(path, scoped_context, owner_object, owner_type, is_eager_selection:, gathered_selections:, after:)
123
163
  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
133
- else
134
- field_ast_nodes = nil
135
- ast_node = field_ast_nodes_or_ast_node
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?
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
+ # @return [void]
199
+ def evaluate_selection(path, result_name, field_ast_nodes_or_ast_node, scoped_context, owner_object, owner_type, is_eager_field)
200
+ # As a performance optimization, the hash key will be a `Node` if
201
+ # there's only one selection of the field. But if there are multiple
202
+ # selections of the field, it will be an Array of nodes
203
+ if field_ast_nodes_or_ast_node.is_a?(Array)
204
+ field_ast_nodes = field_ast_nodes_or_ast_node
205
+ ast_node = field_ast_nodes.first
206
+ else
207
+ field_ast_nodes = nil
208
+ ast_node = field_ast_nodes_or_ast_node
209
+ end
210
+ field_name = ast_node.name
211
+ field_defn = @fields_cache[owner_type][field_name] ||= owner_type.get_field(field_name)
212
+ is_introspection = false
213
+ if field_defn.nil?
214
+ field_defn = if owner_type == schema.query && (entry_point_field = schema.introspection_system.entry_point(name: field_name))
215
+ is_introspection = true
216
+ entry_point_field
217
+ elsif (dynamic_field = schema.introspection_system.dynamic_field(name: field_name))
218
+ is_introspection = true
219
+ dynamic_field
220
+ else
221
+ raise "Invariant: no field for #{owner_type}.#{field_name}"
222
+ end
223
+ end
224
+ return_type = field_defn.type
156
225
 
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)
226
+ next_path = path.dup
227
+ next_path << result_name
228
+ next_path.freeze
163
229
 
164
- context.scoped_context = scoped_context
165
- object = owner_object
230
+ # This seems janky, but we need to know
231
+ # the field's return type at this path in order
232
+ # to propagate `null`
233
+ set_type_at_path(next_path, return_type)
234
+ # Set this before calling `run_with_directives`, so that the directive can have the latest path
235
+ set_all_interpreter_context(nil, field_defn, nil, next_path)
166
236
 
167
- if is_introspection
168
- object = authorized_new(field_defn.owner, object, context, next_path)
169
- end
237
+ context.scoped_context = scoped_context
238
+ object = owner_object
170
239
 
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)
240
+ if is_introspection
241
+ object = authorized_new(field_defn.owner, object, context, next_path)
242
+ end
243
+
244
+ begin
245
+ kwarg_arguments = arguments(object, field_defn, ast_node)
246
+ rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => e
247
+ continue_value(next_path, e, owner_type, field_defn, return_type.non_null?, ast_node)
248
+ return
249
+ end
250
+
251
+ 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|
252
+ if resolved_arguments.is_a?(GraphQL::ExecutionError) || resolved_arguments.is_a?(GraphQL::UnauthorizedError)
253
+ continue_value(next_path, resolved_arguments, owner_type, field_defn, return_type.non_null?, ast_node)
175
254
  next
176
255
  end
177
256
 
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)
257
+ kwarg_arguments = if resolved_arguments.empty? && field_defn.extras.empty?
258
+ # We can avoid allocating the `{ Symbol => Object }` hash in this case
259
+ NO_ARGS
260
+ else
261
+ # Bundle up the extras, then make a new arguments instance
262
+ # that includes the extras, too.
263
+ extra_args = {}
264
+ field_defn.extras.each do |extra|
265
+ case extra
266
+ when :ast_node
267
+ extra_args[:ast_node] = ast_node
268
+ when :execution_errors
269
+ extra_args[:execution_errors] = ExecutionErrors.new(context, ast_node, next_path)
270
+ when :path
271
+ extra_args[:path] = next_path
272
+ when :lookahead
273
+ if !field_ast_nodes
274
+ field_ast_nodes = [ast_node]
212
275
  end
276
+
277
+ extra_args[:lookahead] = Execution::Lookahead.new(
278
+ query: query,
279
+ ast_nodes: field_ast_nodes,
280
+ field: field_defn,
281
+ )
282
+ when :argument_details
283
+ # Use this flag to tell Interpreter::Arguments to add itself
284
+ # to the keyword args hash _before_ freezing everything.
285
+ extra_args[:argument_details] = :__arguments_add_self
286
+ else
287
+ extra_args[extra] = field_defn.fetch_extra(extra, context)
213
288
  end
214
289
  end
290
+ resolved_arguments = resolved_arguments.merge_extras(extra_args)
291
+ resolved_arguments.keyword_arguments
292
+ end
215
293
 
216
- set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
294
+ set_all_interpreter_context(nil, nil, kwarg_arguments, nil)
217
295
 
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
296
+ # Optimize for the case that field is selected only once
297
+ if field_ast_nodes.nil? || field_ast_nodes.size == 1
298
+ next_selections = ast_node.selections
299
+ else
300
+ next_selections = []
301
+ field_ast_nodes.each { |f| next_selections.concat(f.selections) }
302
+ end
225
303
 
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
304
+ field_result = resolve_with_directives(object, ast_node) do
305
+ # Actually call the field resolver and capture the result
306
+ app_result = begin
307
+ query.with_error_handling do
308
+ 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
309
+ field_defn.resolve(object, kwarg_arguments, context)
233
310
  end
234
- rescue GraphQL::ExecutionError => err
235
- err
236
311
  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
312
+ rescue GraphQL::ExecutionError => err
313
+ err
314
+ end
315
+ 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|
316
+ continue_value = continue_value(next_path, inner_result, owner_type, field_defn, return_type.non_null?, ast_node)
317
+ if RawValue === continue_value
318
+ # Write raw value directly to the response without resolving nested objects
319
+ write_in_response(next_path, continue_value.resolve)
320
+ elsif HALT != continue_value
321
+ continue_field(next_path, continue_value, owner_type, field_defn, return_type, ast_node, next_selections, false, object, kwarg_arguments)
245
322
  end
246
323
  end
324
+ end
247
325
 
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
326
+ # If this field is a root mutation field, immediately resolve
327
+ # all of its child fields before moving on to the next root mutation field.
328
+ # (Subselections of this mutation will still be resolved level-by-level.)
329
+ if is_eager_field
330
+ Interpreter::Resolve.resolve_all([field_result])
256
331
  end
332
+
333
+ nil
257
334
  end
258
335
  end
259
336
 
@@ -314,7 +391,7 @@ module GraphQL
314
391
  resolved_type_or_lazy, resolved_value = resolve_type(current_type, value, path)
315
392
  resolved_value ||= value
316
393
 
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|
394
+ 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
395
  possible_types = query.possible_types(current_type)
319
396
 
320
397
  if !possible_types.include?(resolved_type)
@@ -334,12 +411,13 @@ module GraphQL
334
411
  rescue GraphQL::ExecutionError => err
335
412
  err
336
413
  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|
414
+ 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
415
  continue_value = continue_value(path, inner_object, owner_type, field, is_non_null, ast_node)
339
416
  if HALT != continue_value
340
417
  response_hash = {}
341
418
  write_in_response(path, response_hash)
342
- evaluate_selections(path, context.scoped_context, continue_value, current_type, next_selections)
419
+ gathered_selections = gather_selections(continue_value, current_type, next_selections)
420
+ evaluate_selections(path, context.scoped_context, continue_value, current_type, is_eager_selection: false, gathered_selections: gathered_selections, after: nil)
343
421
  response_hash
344
422
  end
345
423
  end
@@ -357,7 +435,7 @@ module GraphQL
357
435
  idx += 1
358
436
  set_type_at_path(next_path, inner_type)
359
437
  # 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|
438
+ 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
439
  continue_value = continue_value(next_path, inner_inner_value, owner_type, field, inner_type.non_null?, ast_node)
362
440
  if HALT != continue_value
363
441
  continue_field(next_path, continue_value, owner_type, field, inner_type, ast_node, next_selections, false, owner_object, arguments)
@@ -440,7 +518,7 @@ module GraphQL
440
518
  # @param eager [Boolean] Set to `true` for mutation root fields only
441
519
  # @param trace [Boolean] If `false`, don't wrap this with field tracing
442
520
  # @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)
521
+ def after_lazy(lazy_obj, owner:, field:, path:, scoped_context:, owner_object:, arguments:, ast_node:, eager: false, trace: true, &block)
444
522
  set_all_interpreter_context(owner_object, field, arguments, path)
445
523
  if schema.lazy?(lazy_obj)
446
524
  lazy = GraphQL::Execution::Lazy.new(path: path, field: field) do
@@ -451,7 +529,7 @@ module GraphQL
451
529
  inner_obj = begin
452
530
  query.with_error_handling do
453
531
  if trace
454
- query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments}) do
532
+ query.trace("execute_field_lazy", {owner: owner, field: field, path: path, query: query, object: owner_object, arguments: arguments, ast_node: ast_node}) do
455
533
  schema.sync_lazy(lazy_obj)
456
534
  end
457
535
  else
@@ -461,7 +539,7 @@ module GraphQL
461
539
  rescue GraphQL::ExecutionError, GraphQL::UnauthorizedError => err
462
540
  err
463
541
  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)
542
+ 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
543
  end
466
544
 
467
545
  if eager
@@ -476,9 +554,7 @@ module GraphQL
476
554
  end
477
555
 
478
556
  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?))
557
+ if arg_owner.arguments_statically_coercible?
482
558
  query.arguments_for(ast_node, arg_owner)
483
559
  else
484
560
  # The arguments must be prepared in the context of the given object
@@ -519,6 +595,16 @@ module GraphQL
519
595
  end
520
596
  end
521
597
 
598
+ def value_at(path)
599
+ i = 0
600
+ value = @response.final_value
601
+ while value && (part = path[i])
602
+ value = value[part]
603
+ i += 1
604
+ end
605
+ value
606
+ end
607
+
522
608
  # To propagate nulls, we have to know what the field type was
523
609
  # at previous parts of the response.
524
610
  # This hash matches the response