graphql 1.6.2 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +18 -1
  3. data/lib/graphql/analysis/query_complexity.rb +1 -1
  4. data/lib/graphql/backwards_compatibility.rb +1 -1
  5. data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -2
  6. data/lib/graphql/execution/execute.rb +12 -3
  7. data/lib/graphql/execution/lazy.rb +7 -2
  8. data/lib/graphql/execution/lazy/lazy_method_map.rb +1 -1
  9. data/lib/graphql/execution/lazy/resolve.rb +38 -15
  10. data/lib/graphql/execution/multiplex.rb +48 -35
  11. data/lib/graphql/field.rb +11 -1
  12. data/lib/graphql/internal_representation/node.rb +3 -1
  13. data/lib/graphql/non_null_type.rb +1 -1
  14. data/lib/graphql/query.rb +1 -1
  15. data/lib/graphql/query/arguments.rb +5 -3
  16. data/lib/graphql/query/arguments_cache.rb +10 -6
  17. data/lib/graphql/query/context.rb +18 -8
  18. data/lib/graphql/query/null_context.rb +1 -1
  19. data/lib/graphql/query/variables.rb +1 -1
  20. data/lib/graphql/relay.rb +1 -0
  21. data/lib/graphql/relay/base_connection.rb +7 -9
  22. data/lib/graphql/relay/connection_type.rb +6 -15
  23. data/lib/graphql/relay/edges_instrumentation.rb +56 -0
  24. data/lib/graphql/schema.rb +2 -1
  25. data/lib/graphql/schema/middleware_chain.rb +2 -11
  26. data/lib/graphql/schema/type_map.rb +1 -1
  27. data/lib/graphql/static_validation/definition_dependencies.rb +1 -1
  28. data/lib/graphql/static_validation/validation_context.rb +1 -1
  29. data/lib/graphql/version.rb +1 -1
  30. data/spec/graphql/execution/multiplex_spec.rb +4 -0
  31. data/spec/graphql/query_spec.rb +24 -1
  32. data/spec/support/dummy/data.rb +7 -1
  33. data/spec/support/star_wars/schema.rb +18 -1
  34. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dcc96523b4e6938e39aeae90c15728985618c95b
4
- data.tar.gz: da73ad353f7891bf9f4146e8941200747a928c39
3
+ metadata.gz: b8eca19d9b215aed67ef5434bb5dab5cce3e8626
4
+ data.tar.gz: 66740ce037cbd8e4019ed7516503e430ebb0a34a
5
5
  SHA512:
6
- metadata.gz: b709b3b6833dd60102f6bfa7ebc7efd7a1a36ef7af8b0aa5723b1b421cc4c14516ef2480d058c885d9fb9de34013c0e3d0e6d0bbaa22850df7ff2bf9c50535c8
7
- data.tar.gz: 0a4b99378090cfbce6916a725ccadb8b91238cbeaf8a00c28e0fcbcedc43d1b8c238b3f2c502ece948e5379a2d33cb2265e8210c2e44ae466d570f177fb44059
6
+ metadata.gz: 6e34b8bce773c1304d9c4796a20a92c731de182ad1a144df2bb4a814295a2233b298d8817d24c96837540b57fd73ca3a9acb55b286308c1b7a3b3725f31c3e11
7
+ data.tar.gz: b9cce7a863dfaabcb7312a0844ef659504cf5adedfe7b22246ce4856ae78d71c6204535a1351b5d0c746161b5155d06cd25f845a0d8fdc3776a9ee27ed41160d
@@ -3,9 +3,26 @@ require "delegate"
3
3
  require "json"
4
4
  require "set"
5
5
  require "singleton"
6
- require "forwardable"
7
6
 
8
7
  module GraphQL
8
+ # Ruby stdlib was pretty busted until this fix:
9
+ # https://github.com/ruby/ruby/commit/46c0e79bb5b96c45c166ef62f8e585f528862abb#diff-43adf0e587a50dbaf51764a262008d40
10
+ module Delegate
11
+ def def_delegators(accessor, *method_names)
12
+ method_names.each do |method_name|
13
+ class_eval <<-RUBY
14
+ def #{method_name}(*args)
15
+ if block_given?
16
+ #{accessor}.#{method_name}(*args, &Proc.new)
17
+ else
18
+ #{accessor}.#{method_name}(*args)
19
+ end
20
+ end
21
+ RUBY
22
+ end
23
+ end
24
+ end
25
+
9
26
  class Error < StandardError
10
27
  end
11
28
 
@@ -69,7 +69,7 @@ module GraphQL
69
69
  # Find the maximum possible complexity among those combinations.
70
70
  class TypeComplexity
71
71
  def initialize
72
- @types = Hash.new { |h, k| h[k] = 0 }
72
+ @types = Hash.new(0)
73
73
  end
74
74
 
75
75
  # Return the max possible complexity for types in this selection
@@ -25,7 +25,7 @@ module GraphQL
25
25
 
26
26
  def get_arity(callable)
27
27
  case callable
28
- when Proc
28
+ when Method, Proc
29
29
  callable.arity
30
30
  else
31
31
  callable.method(:call).arity
@@ -66,9 +66,9 @@ module GraphQL
66
66
  |
67
67
  res = self.class.lazy_schema.execute(query_str, context: {pushes: []})
68
68
  assert_equal nil, res["data"]
69
- assert_equal 2, res["errors"].length
69
+ # The first fail causes the second field to never resolve
70
+ assert_equal 1, res["errors"].length
70
71
  assert_equal ["push", "push", "fail1", "value"], res["errors"][0]["path"]
71
- assert_equal ["push", "push", "fail2", "value"], res["errors"][1]["path"]
72
72
  end
73
73
 
74
74
  def test_it_resolves_mutation_values_eagerly
@@ -4,8 +4,14 @@ module GraphQL
4
4
  # A valid execution strategy
5
5
  # @api private
6
6
  class Execute
7
+
7
8
  # @api private
8
- SKIP = Object.new
9
+ class Skip; end
10
+
11
+ # Just a singleton for implementing {Query::Context#skip}
12
+ # @api private
13
+ SKIP = Skip.new
14
+
9
15
  # @api private
10
16
  PROPAGATE_NULL = Object.new
11
17
 
@@ -40,7 +46,7 @@ module GraphQL
40
46
  query_ctx
41
47
  )
42
48
 
43
- if field_result == SKIP
49
+ if field_result.is_a?(Skip)
44
50
  next
45
51
  end
46
52
 
@@ -103,6 +109,9 @@ module GraphQL
103
109
  end
104
110
 
105
111
  def continue_resolve_field(owner, selection, parent_type, field, raw_value, field_ctx)
112
+ if owner.invalid_null?
113
+ return
114
+ end
106
115
  query = field_ctx.query
107
116
 
108
117
  case raw_value
@@ -147,7 +156,7 @@ module GraphQL
147
156
  else
148
157
  nil
149
158
  end
150
- elsif value == SKIP
159
+ elsif value.is_a?(Skip)
151
160
  value
152
161
  else
153
162
  case field_type.kind
@@ -40,11 +40,16 @@ module GraphQL
40
40
  end
41
41
 
42
42
  # @return [Lazy] A {Lazy} whose value depends on another {Lazy}, plus any transformations in `block`
43
- def then(&block)
43
+ def then
44
44
  self.class.new {
45
- block.call(value)
45
+ yield(value)
46
46
  }
47
47
  end
48
+
49
+ # This can be used for fields which _had no_ lazy results
50
+ # @api private
51
+ NullResult = Lazy.new(){}
52
+ NullResult.value
48
53
  end
49
54
  end
50
55
  end
@@ -51,7 +51,7 @@ module GraphQL
51
51
 
52
52
  # Mock the Concurrent::Map API
53
53
  class ConcurrentishMap
54
- extend Forwardable
54
+ extend GraphQL::Delegate
55
55
  # Technically this should be under the mutex too,
56
56
  # but I know it's only used when the lock is already acquired.
57
57
  def_delegators :@storage, :each_pair, :size
@@ -7,46 +7,69 @@ module GraphQL
7
7
  module Resolve
8
8
  # Mutate `value`, replacing {Lazy} instances in place with their resolved values
9
9
  # @return [void]
10
+
11
+ # This object can be passed like an array, but it doesn't allocate an
12
+ # array until it's used.
13
+ #
14
+ # There's one crucial difference: you have to _capture_ the result
15
+ # of `#<<`. (This _works_ with arrays but isn't required, since it has a side-effect.)
16
+ # @api private
17
+ module NullAccumulator
18
+ def self.<<(item)
19
+ [item]
20
+ end
21
+
22
+ def self.empty?
23
+ true
24
+ end
25
+ end
26
+
10
27
  def self.resolve(value)
11
28
  lazies = resolve_in_place(value)
12
29
  deep_sync(lazies)
13
30
  end
14
31
 
15
32
  def self.resolve_in_place(value)
16
- lazies = []
33
+ acc = each_lazy(NullAccumulator, value)
17
34
 
18
- each_lazy(value) do |field_result|
19
- inner_lazy = field_result.value.then do |inner_v|
20
- field_result.value = inner_v
21
- resolve_in_place(inner_v)
35
+ if acc.empty?
36
+ Lazy::NullResult
37
+ else
38
+ acc.each_with_index do |field_result, idx|
39
+ acc[idx] = field_result.value.then do |inner_v|
40
+ field_result.value = inner_v
41
+ resolve_in_place(inner_v)
42
+ end
22
43
  end
23
- lazies.push(inner_lazy)
24
- end
25
44
 
26
- Lazy.new { lazies.map(&:value) }
45
+ Lazy.new { acc.each_with_index { |l, idx| acc[idx] = l.value }; acc }
46
+ end
27
47
  end
28
48
 
29
- # If `value` is a collection, call `block`
30
- # with any {Lazy} instances in the collection
49
+ # If `value` is a collection,
50
+ # add any {Lazy} instances in the collection
51
+ # to `acc`
31
52
  # @return [void]
32
- def self.each_lazy(value, &block)
53
+ def self.each_lazy(acc, value)
33
54
  case value
34
55
  when SelectionResult
35
56
  value.each do |key, field_result|
36
- each_lazy(field_result, &block)
57
+ acc = each_lazy(acc, field_result)
37
58
  end
38
59
  when Array
39
60
  value.each do |field_result|
40
- each_lazy(field_result, &block)
61
+ acc = each_lazy(acc, field_result)
41
62
  end
42
63
  when FieldResult
43
64
  field_value = value.value
44
65
  if field_value.is_a?(Lazy)
45
- yield(value)
66
+ acc = acc << value
46
67
  else
47
- each_lazy(field_value, &block)
68
+ acc = each_lazy(acc, field_value)
48
69
  end
49
70
  end
71
+
72
+ acc
50
73
  end
51
74
 
52
75
  # Traverse `val`, triggering resolution for each {Lazy}.
@@ -46,39 +46,25 @@ module GraphQL
46
46
  # @param max_complexity [Integer]
47
47
  # @return [Array<Hash>] One result per query
48
48
  def run_queries(schema, queries, context: {}, max_complexity: nil)
49
- has_custom_strategy = schema.query_execution_strategy || schema.mutation_execution_strategy || schema.subscription_execution_strategy
50
- if has_custom_strategy
51
- if queries.length == 1
52
- return [run_one_legacy(schema, queries.first)]
53
- else
49
+ if has_custom_strategy?(schema)
50
+ if queries.length != 1
54
51
  raise ArgumentError, "Multiplexing doesn't support custom execution strategies, run one query at a time instead"
52
+ else
53
+ with_instrumentation(schema, queries, context: context, max_complexity: max_complexity) do
54
+ [run_one_legacy(schema, queries.first)]
55
+ end
55
56
  end
56
57
  else
57
- run_as_multiplex(schema, queries, context: context, max_complexity: max_complexity)
58
+ with_instrumentation(schema, queries, context: context, max_complexity: max_complexity) do
59
+ run_as_multiplex(queries)
60
+ end
58
61
  end
59
62
  end
60
63
 
61
64
  private
62
65
 
63
- def run_as_multiplex(schema, queries, context:, max_complexity:)
64
- query_instrumenters = schema.instrumenters[:query]
65
- multiplex_instrumenters = schema.instrumenters[:multiplex]
66
- multiplex = self.new(schema: schema, queries: queries, context: context)
67
-
68
- # First, run multiplex instrumentation, then query instrumentation for each query
69
- multiplex_instrumenters.each { |i| i.before_multiplex(multiplex) }
70
- queries.each do |query|
71
- query_instrumenters.each { |i| i.before_query(query) }
72
- end
73
-
74
- multiplex_analyzers = schema.multiplex_analyzers
75
- if max_complexity ||= schema.max_complexity
76
- multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity.new(max_complexity)]
77
- end
78
-
79
- GraphQL::Analysis.analyze_multiplex(multiplex, multiplex_analyzers)
80
-
81
- # Then, do as much eager evaluation of the query as possible
66
+ def run_as_multiplex(queries)
67
+ # Do as much eager evaluation of the query as possible
82
68
  results = queries.map do |query|
83
69
  begin_query(query)
84
70
  end
@@ -91,13 +77,6 @@ module GraphQL
91
77
  query = queries[idx]
92
78
  finish_query(data_result, query)
93
79
  end
94
- ensure
95
- # Finally, run teardown instrumentation for each query + the multiplex
96
- # Use `reverse_each` so instrumenters are treated like a stack
97
- queries.each do |query|
98
- query_instrumenters.reverse_each { |i| i.after_query(query) }
99
- end
100
- multiplex_instrumenters.reverse_each { |i| i.after_multiplex(multiplex) }
101
80
  end
102
81
 
103
82
  # @param query [GraphQL::Query]
@@ -149,8 +128,6 @@ module GraphQL
149
128
 
150
129
  # use the old `query_execution_strategy` etc to run this query
151
130
  def run_one_legacy(schema, query)
152
- instrumenters = schema.instrumenters[:query]
153
- instrumenters.each { |i| i.before_query(query) }
154
131
  query.result = if !query.valid?
155
132
  all_errors = query.validation_errors + query.analysis_errors + query.context.errors
156
133
  if all_errors.any?
@@ -161,8 +138,44 @@ module GraphQL
161
138
  else
162
139
  GraphQL::Query::Executor.new(query).result
163
140
  end
141
+ end
142
+
143
+ def has_custom_strategy?(schema)
144
+ schema.query_execution_strategy != GraphQL::Execution::Execute ||
145
+ schema.mutation_execution_strategy != GraphQL::Execution::Execute ||
146
+ schema.subscription_execution_strategy != GraphQL::Execution::Execute
147
+ end
148
+
149
+ # Apply multiplex & query instrumentation to `queries`.
150
+ #
151
+ # It yields when the queries should be executed, then runs teardown.
152
+ def with_instrumentation(schema, queries, context:, max_complexity:)
153
+ query_instrumenters = schema.instrumenters[:query]
154
+ multiplex_instrumenters = schema.instrumenters[:multiplex]
155
+ multiplex = self.new(schema: schema, queries: queries, context: context)
156
+
157
+ # First, run multiplex instrumentation, then query instrumentation for each query
158
+ multiplex_instrumenters.each { |i| i.before_multiplex(multiplex) }
159
+ queries.each do |query|
160
+ query_instrumenters.each { |i| i.before_query(query) }
161
+ end
162
+
163
+ multiplex_analyzers = schema.multiplex_analyzers
164
+ if max_complexity ||= schema.max_complexity
165
+ multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity.new(max_complexity)]
166
+ end
167
+
168
+ GraphQL::Analysis.analyze_multiplex(multiplex, multiplex_analyzers)
169
+
170
+ # Let them be executed
171
+ yield
164
172
  ensure
165
- instrumenters.reverse_each { |i| i.after_query(query) }
173
+ # Finally, run teardown instrumentation for each query + the multiplex
174
+ # Use `reverse_each` so instrumenters are treated like a stack
175
+ queries.each do |query|
176
+ query_instrumenters.reverse_each { |i| i.after_query(query) }
177
+ end
178
+ multiplex_instrumenters.reverse_each { |i| i.after_multiplex(multiplex) }
166
179
  end
167
180
  end
168
181
  end
@@ -126,6 +126,7 @@ module GraphQL
126
126
  :type, :arguments,
127
127
  :property, :hash_key, :complexity,
128
128
  :mutation, :function,
129
+ :edge_class,
129
130
  :relay_node_field,
130
131
  :relay_nodes_field,
131
132
  argument: GraphQL::Define::AssignArgument
@@ -135,7 +136,7 @@ module GraphQL
135
136
  :mutation, :arguments, :complexity, :function,
136
137
  :resolve, :resolve=, :lazy_resolve, :lazy_resolve=, :lazy_resolve_proc, :resolve_proc,
137
138
  :type, :type=, :name=, :property=, :hash_key=,
138
- :relay_node_field, :relay_nodes_field
139
+ :relay_node_field, :relay_nodes_field, :edges?, :edge_class
139
140
  )
140
141
 
141
142
  # @return [Boolean] True if this is the Relay find-by-id field
@@ -184,6 +185,15 @@ module GraphQL
184
185
  @connection
185
186
  end
186
187
 
188
+ # @return [nil, Class]
189
+ # @api private
190
+ attr_accessor :edge_class
191
+
192
+ # @return [Boolean]
193
+ def edges?
194
+ !!@edge_class
195
+ end
196
+
187
197
  # @return [nil, Integer]
188
198
  attr_accessor :connection_max_page_size
189
199
 
@@ -2,6 +2,8 @@
2
2
  module GraphQL
3
3
  module InternalRepresentation
4
4
  class Node
5
+ # @api private
6
+ DEFAULT_TYPED_CHILDREN = Proc.new { |h, k| h[k] = {} }
5
7
  # @return [String] the name this node has in the response
6
8
  attr_reader :name
7
9
 
@@ -15,7 +17,7 @@ module GraphQL
15
17
  # @return [Hash<GraphQL::ObjectType, Hash<String => Node>>]
16
18
  def typed_children
17
19
  @typed_childen ||= begin
18
- new_tc = Hash.new { |h, k| h[k] = {} }
20
+ new_tc = Hash.new(&DEFAULT_TYPED_CHILDREN)
19
21
  if @scoped_children.any?
20
22
  all_object_types = Set.new
21
23
  scoped_children.each_key { |t| all_object_types.merge(@query.possible_types(t)) }
@@ -30,7 +30,7 @@ module GraphQL
30
30
  #
31
31
  class NonNullType < GraphQL::BaseType
32
32
  include GraphQL::BaseType::ModifiesAnotherType
33
- extend Forwardable
33
+ extend GraphQL::Delegate
34
34
 
35
35
  attr_reader :of_type
36
36
  def initialize(of_type:)
@@ -14,7 +14,7 @@ require "graphql/query/validation_pipeline"
14
14
  module GraphQL
15
15
  # A combination of query string and {Schema} instance which can be reduced to a {#result}.
16
16
  class Query
17
- extend Forwardable
17
+ extend GraphQL::Delegate
18
18
 
19
19
  class OperationNameMissingError < GraphQL::ExecutionError
20
20
  def initialize(name)
@@ -5,7 +5,7 @@ module GraphQL
5
5
  #
6
6
  # {Arguments} recursively wraps the input in {Arguments} instances.
7
7
  class Arguments
8
- extend Forwardable
8
+ extend GraphQL::Delegate
9
9
 
10
10
  def initialize(values, argument_definitions:)
11
11
  @argument_values = values.inject({}) do |memo, (inner_key, inner_value)|
@@ -21,13 +21,15 @@ module GraphQL
21
21
  # @param key [String, Symbol] name or index of value to access
22
22
  # @return [Object] the argument at that key
23
23
  def [](key)
24
- @argument_values.fetch(key.to_s, NULL_ARGUMENT_VALUE).value
24
+ key_s = key.is_a?(String) ? key : key.to_s
25
+ @argument_values.fetch(key_s, NULL_ARGUMENT_VALUE).value
25
26
  end
26
27
 
27
28
  # @param key [String, Symbol] name of value to access
28
29
  # @return [Boolean] true if the argument was present in this field
29
30
  def key?(key)
30
- @argument_values.key?(key.to_s)
31
+ key_s = key.is_a?(String) ? key : key.to_s
32
+ @argument_values.key?(key_s)
31
33
  end
32
34
 
33
35
  # Get the hash of all values, with stringified keys
@@ -5,14 +5,18 @@ module GraphQL
5
5
  # @return [Hash<InternalRepresentation::Node, GraphQL::Language::NodesDirectiveNode => Hash<GraphQL::Field, GraphQL::Directive => GraphQL::Query::Arguments>>]
6
6
  def self.build(query)
7
7
  Hash.new do |h1, irep_or_ast_node|
8
- Hash.new do |h2, definition|
8
+ h1[irep_or_ast_node] = Hash.new do |h2, definition|
9
9
  ast_node = irep_or_ast_node.is_a?(GraphQL::InternalRepresentation::Node) ? irep_or_ast_node.ast_node : irep_or_ast_node
10
10
  ast_arguments = ast_node.arguments
11
- GraphQL::Query::LiteralInput.from_arguments(
12
- ast_arguments,
13
- definition.arguments,
14
- query.variables,
15
- )
11
+ h2[definition] = if definition.arguments.none?
12
+ GraphQL::Query::Arguments::NO_ARGS
13
+ else
14
+ GraphQL::Query::LiteralInput.from_arguments(
15
+ ast_arguments,
16
+ definition.arguments,
17
+ query.variables,
18
+ )
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -4,7 +4,7 @@ module GraphQL
4
4
  # Expose some query-specific info to field resolve functions.
5
5
  # It delegates `[]` to the hash that's passed to `GraphQL::Query#initialize`.
6
6
  class Context
7
- extend Forwardable
7
+ extend GraphQL::Delegate
8
8
  attr_reader :execution_strategy
9
9
  # `strategy` is required by GraphQL::Batch
10
10
  alias_method :strategy, :execution_strategy
@@ -73,7 +73,8 @@ module GraphQL
73
73
  def spawn(key:, selection:, parent_type:, field:)
74
74
  FieldResolutionContext.new(
75
75
  context: self,
76
- path: path + [key],
76
+ parent: self,
77
+ key: key,
77
78
  selection: selection,
78
79
  parent_type: parent_type,
79
80
  field: field,
@@ -87,21 +88,29 @@ module GraphQL
87
88
  end
88
89
 
89
90
  class FieldResolutionContext
90
- extend Forwardable
91
+ extend GraphQL::Delegate
91
92
 
92
- attr_reader :path, :selection, :field, :parent_type
93
+ attr_reader :selection, :field, :parent_type, :query, :schema
93
94
 
94
- def initialize(context:, path:, selection:, field:, parent_type:)
95
+ def initialize(context:, key:, selection:, parent:, field:, parent_type:)
95
96
  @context = context
96
- @path = path
97
+ @key = key
98
+ @parent = parent
97
99
  @selection = selection
98
100
  @field = field
99
101
  @parent_type = parent_type
102
+ # This is needed constantly, so set it ahead of time:
103
+ @query = context.query
104
+ @schema = context.schema
105
+ end
106
+
107
+ def path
108
+ @path ||= @parent.path.dup << @key
100
109
  end
101
110
 
102
111
  def_delegators :@context,
103
112
  :[], :[]=, :key?, :fetch, :to_h, :namespace,
104
- :spawn, :query, :schema, :warden, :errors,
113
+ :spawn, :schema, :warden, :errors,
105
114
  :execution_strategy, :strategy, :skip
106
115
 
107
116
  # @return [GraphQL::Language::Nodes::Field] The AST node for the currently-executing field
@@ -131,7 +140,8 @@ module GraphQL
131
140
  def spawn(key:, selection:, parent_type:, field:)
132
141
  FieldResolutionContext.new(
133
142
  context: @context,
134
- path: path + [key],
143
+ parent: self,
144
+ key: key,
135
145
  selection: selection,
136
146
  parent_type: parent_type,
137
147
  field: field,
@@ -16,7 +16,7 @@ module GraphQL
16
16
  end
17
17
 
18
18
  class << self
19
- extend Forwardable
19
+ extend GraphQL::Delegate
20
20
 
21
21
  def instance
22
22
  @instance = self.new
@@ -3,7 +3,7 @@ module GraphQL
3
3
  class Query
4
4
  # Read-only access to query variables, applying default values if needed.
5
5
  class Variables
6
- extend Forwardable
6
+ extend GraphQL::Delegate
7
7
 
8
8
  # @return [Array<GraphQL::Query::VariableValidationError>] Any errors encountered when parsing the provided variables and literal values
9
9
  attr_reader :errors
@@ -4,6 +4,7 @@ require 'base64'
4
4
  require 'graphql/relay/page_info'
5
5
  require 'graphql/relay/edge'
6
6
  require 'graphql/relay/edge_type'
7
+ require 'graphql/relay/edges_instrumentation'
7
8
  require 'graphql/relay/base_connection'
8
9
  require 'graphql/relay/array_connection'
9
10
  require 'graphql/relay/range_add'
@@ -26,16 +26,14 @@ module GraphQL
26
26
  # @return [subclass of BaseConnection] a connection Class for wrapping `nodes`
27
27
  def connection_for_nodes(nodes)
28
28
  # Check for class _names_ because classes can be redefined in Rails development
29
- ancestor_names = nodes.class.ancestors.map(&:name)
30
- implementation_class_name = ancestor_names.find do |ancestor_class_name|
31
- CONNECTION_IMPLEMENTATIONS.include? ancestor_class_name
32
- end
33
-
34
- if implementation_class_name.nil?
35
- raise("No connection implementation to wrap #{nodes.class} (#{nodes})")
36
- else
37
- CONNECTION_IMPLEMENTATIONS[implementation_class_name]
29
+ nodes.class.ancestors.each do |ancestor|
30
+ conn_impl = CONNECTION_IMPLEMENTATIONS[ancestor.name]
31
+ if conn_impl
32
+ return conn_impl
33
+ end
38
34
  end
35
+ # Should have found a connection during the loop:
36
+ raise("No connection implementation to wrap #{nodes.class} (#{nodes})")
39
37
  end
40
38
 
41
39
  # Add `connection_class` as the connection wrapper for `nodes_class`
@@ -9,9 +9,8 @@ module GraphQL
9
9
  self.default_nodes_field = false
10
10
 
11
11
  # Create a connection which exposes edges of this type
12
- def self.create_type(wrapped_type, edge_type: nil, edge_class: nil, nodes_field: ConnectionType.default_nodes_field, &block)
13
- edge_class ||= GraphQL::Relay::Edge
14
- edge_type ||= wrapped_type.edge_type
12
+ def self.create_type(wrapped_type, edge_type: wrapped_type.edge_type, edge_class: GraphQL::Relay::Edge, nodes_field: ConnectionType.default_nodes_field, &block)
13
+ custom_edge_class = edge_class
15
14
 
16
15
  # Any call that would trigger `wrapped_type.ensure_defined`
17
16
  # must be inside this lazy block, otherwise we get weird
@@ -19,20 +18,12 @@ module GraphQL
19
18
  ObjectType.define do
20
19
  name("#{wrapped_type.name}Connection")
21
20
  description("The connection type for #{wrapped_type.name}.")
22
- field :edges, types[edge_type] do
23
- description "A list of edges."
24
- resolve ->(obj, args, ctx) {
25
- obj.edge_nodes.map { |item| edge_class.new(item, obj) }
26
- }
27
- end
21
+ field :edges, types[edge_type], "A list of edges.", edge_class: custom_edge_class, property: :edge_nodes
22
+
28
23
  if nodes_field
29
- field :nodes, types[wrapped_type] do
30
- description "A list of nodes."
31
- resolve ->(obj, args, ctx) {
32
- obj.edge_nodes
33
- }
34
- end
24
+ field :nodes, types[wrapped_type], "A list of nodes.", property: :edge_nodes
35
25
  end
26
+
36
27
  field :pageInfo, !PageInfo, "Information to aid in pagination.", property: :page_info
37
28
  block && instance_eval(&block)
38
29
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Relay
4
+ module EdgesInstrumentation
5
+ def self.instrument(type, field)
6
+ if field.edges?
7
+ edges_resolve = EdgesResolve.new(
8
+ edge_class: field.edge_class,
9
+ resolve: field.resolve_proc,
10
+ lazy_resolve: field.lazy_resolve_proc,
11
+ )
12
+
13
+ field.redefine(
14
+ resolve: edges_resolve.method(:resolve),
15
+ lazy_resolve: edges_resolve.method(:lazy_resolve),
16
+ )
17
+ else
18
+ field
19
+ end
20
+ end
21
+
22
+
23
+ class EdgesResolve
24
+ def initialize(edge_class:, resolve:, lazy_resolve:)
25
+ @edge_class = edge_class
26
+ @resolve_proc = resolve
27
+ @lazy_resolve_proc = lazy_resolve
28
+ end
29
+
30
+ # A user's custom Connection may return a lazy object,
31
+ # if so, handle it later.
32
+ def resolve(obj, args, ctx)
33
+ nodes = @resolve_proc.call(obj, args, ctx)
34
+ if ctx.schema.lazy?(nodes)
35
+ ConnectionResolve::LazyNodesWrapper.new(obj, nodes)
36
+ else
37
+ build_edges(nodes, obj)
38
+ end
39
+ end
40
+
41
+ # If we get this far, unwrap the wrapper,
42
+ # resolve the lazy object and make the edges as usual
43
+ def lazy_resolve(obj, args, ctx)
44
+ items = @lazy_resolve_proc.call(obj.lazy_object, args, ctx)
45
+ build_edges(items, obj.parent)
46
+ end
47
+
48
+ private
49
+
50
+ def build_edges(items, connection)
51
+ items.map { |item| @edge_class.new(item, connection) }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -98,7 +98,7 @@ module GraphQL
98
98
  GraphQL::Filter.new(except: default_mask)
99
99
  end
100
100
 
101
- self.default_execution_strategy = nil
101
+ self.default_execution_strategy = GraphQL::Execution::Execute
102
102
 
103
103
  BUILT_IN_TYPES = Hash[[INT_TYPE, STRING_TYPE, FLOAT_TYPE, BOOLEAN_TYPE, ID_TYPE].map{ |type| [type.name, type] }]
104
104
  DIRECTIVES = [GraphQL::Directive::IncludeDirective, GraphQL::Directive::SkipDirective, GraphQL::Directive::DeprecatedDirective]
@@ -509,6 +509,7 @@ module GraphQL
509
509
  # @api private
510
510
  BUILT_IN_INSTRUMENTERS = [
511
511
  GraphQL::Relay::ConnectionInstrumentation,
512
+ GraphQL::Relay::EdgesInstrumentation,
512
513
  GraphQL::Relay::Mutation::Instrumentation,
513
514
  ]
514
515
 
@@ -5,7 +5,7 @@ module GraphQL
5
5
  #
6
6
  # Steps should call `next_step.call` to continue the chain, or _not_ call it to stop the chain.
7
7
  class MiddlewareChain
8
- extend Forwardable
8
+ extend GraphQL::Delegate
9
9
 
10
10
  # @return [Array<#call(*args)>] Steps in this chain, will be called with arguments and `next_middleware`
11
11
  attr_reader :steps, :final_step
@@ -66,22 +66,13 @@ module GraphQL
66
66
  end
67
67
 
68
68
  def wrap(callable)
69
- if get_arity(callable) == 6
69
+ if BackwardsCompatibility.get_arity(callable) == 6
70
70
  warn("Middleware that takes a next_middleware parameter is deprecated (#{callable.inspect}); instead, accept a block and use yield.")
71
71
  MiddlewareWrapper.new(callable)
72
72
  else
73
73
  callable
74
74
  end
75
75
  end
76
-
77
- def get_arity(callable)
78
- case callable
79
- when Proc, Method
80
- callable.arity
81
- else
82
- callable.method(:call).arity
83
- end
84
- end
85
76
  end
86
77
  end
87
78
  end
@@ -8,7 +8,7 @@ module GraphQL
8
8
  #
9
9
  # If you want a type, but want to handle the undefined case, use {#fetch}.
10
10
  class TypeMap
11
- extend Forwardable
11
+ extend GraphQL::Delegate
12
12
  def_delegators :@storage, :key?, :keys, :values, :to_h, :fetch, :each, :each_value
13
13
 
14
14
  def initialize
@@ -103,7 +103,7 @@ module GraphQL
103
103
  end
104
104
 
105
105
  class NodeWithPath
106
- extend Forwardable
106
+ extend GraphQL::Delegate
107
107
  attr_reader :node, :path
108
108
  def initialize(node, path)
109
109
  @node = node
@@ -12,7 +12,7 @@ module GraphQL
12
12
  # It also provides limited access to the {TypeStack} instance,
13
13
  # which tracks state as you climb in and out of different fields.
14
14
  class ValidationContext
15
- extend Forwardable
15
+ extend GraphQL::Delegate
16
16
 
17
17
  attr_reader :query, :schema,
18
18
  :document, :errors, :visitor,
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.6.2"
3
+ VERSION = "1.6.3"
4
4
  end
@@ -64,6 +64,10 @@ describe GraphQL::Execution::Multiplex do
64
64
  nestedSum(value: 13) {
65
65
  value
66
66
  }
67
+ # This field will never get executed
68
+ ns2: nestedSum(value: 13) {
69
+ value
70
+ }
67
71
  }
68
72
  }
69
73
  }" }
@@ -598,7 +598,12 @@ describe GraphQL::Query do
598
598
  end
599
599
 
600
600
  describe "query_execution_strategy" do
601
- let(:custom_execution_schema) { schema.redefine(query_execution_strategy: DummyStrategy) }
601
+ let(:custom_execution_schema) {
602
+ schema.redefine do
603
+ query_execution_strategy DummyStrategy
604
+ instrument(:multiplex, DummyMultiplexInstrumenter)
605
+ end
606
+ }
602
607
 
603
608
  class DummyStrategy
604
609
  def execute(ast_operation, root_type, query_object)
@@ -606,9 +611,27 @@ describe GraphQL::Query do
606
611
  end
607
612
  end
608
613
 
614
+ class DummyMultiplexInstrumenter
615
+ def self.before_multiplex(m)
616
+ m.queries.first.context[:before_multiplex] = true
617
+ end
618
+
619
+ def self.after_multiplex(m)
620
+ end
621
+ end
622
+
609
623
  it "is used for running a query, if it's present and not the default" do
610
624
  result = custom_execution_schema.execute(" { __typename }")
611
625
  assert_equal({"data"=>{"dummy"=>true}}, result)
626
+
627
+ result = custom_execution_schema.execute(" mutation { __typename } ")
628
+ assert_equal({"data"=>{"__typename" => "Mutation"}}, result)
629
+ end
630
+
631
+ it "treats the query as a one-item multiplex" do
632
+ ctx = {}
633
+ custom_execution_schema.execute(" { __typename }", context: ctx)
634
+ assert_equal true, ctx[:before_multiplex]
612
635
  end
613
636
 
614
637
  it "can't run a multiplex" do
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  require 'ostruct'
3
3
  module Dummy
4
- Cheese = Struct.new(:id, :flavor, :origin, :fat_content, :source)
4
+ Cheese = Struct.new(:id, :flavor, :origin, :fat_content, :source) do
5
+ def ==(other)
6
+ # This is buggy on purpose -- it shouldn't be called during execution.
7
+ other.id == id
8
+ end
9
+ end
10
+
5
11
  CHEESES = {
6
12
  1 => Cheese.new(1, "Brie", "France", 0.19, 1),
7
13
  2 => Cheese.new(2, "Gouda", "Netherlands", 0.3, 1),
@@ -178,7 +178,11 @@ module StarWars
178
178
  }
179
179
  end
180
180
 
181
- connection :basesWithCustomEdge, CustomEdgeBaseConnectionType, property: :bases
181
+ connection :basesWithCustomEdge, CustomEdgeBaseConnectionType do
182
+ resolve ->(o, a, c) {
183
+ LazyNodesWrapper.new(o.bases)
184
+ }
185
+ end
182
186
  end
183
187
 
184
188
  # Define a mutation. It will also:
@@ -271,6 +275,19 @@ module StarWars
271
275
  end
272
276
  end
273
277
 
278
+ LazyNodesWrapper = Struct.new(:relation)
279
+ class LazyNodesRelationConnection < GraphQL::Relay::RelationConnection
280
+ def initialize(wrapper, *args)
281
+ super(wrapper.relation, *args)
282
+ end
283
+
284
+ def edge_nodes
285
+ LazyWrapper.new { super }
286
+ end
287
+ end
288
+
289
+ GraphQL::Relay::BaseConnection.register_connection_implementation(LazyNodesWrapper, LazyNodesRelationConnection)
290
+
274
291
  QueryType = GraphQL::ObjectType.define do
275
292
  name "Query"
276
293
  field :rebels, Faction do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.2
4
+ version: 1.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-02 00:00:00.000000000 Z
11
+ date: 2017-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark-ips
@@ -445,6 +445,7 @@ files:
445
445
  - lib/graphql/relay/connection_type.rb
446
446
  - lib/graphql/relay/edge.rb
447
447
  - lib/graphql/relay/edge_type.rb
448
+ - lib/graphql/relay/edges_instrumentation.rb
448
449
  - lib/graphql/relay/global_id_resolve.rb
449
450
  - lib/graphql/relay/mutation.rb
450
451
  - lib/graphql/relay/mutation/instrumentation.rb