graphql 1.6.2 → 1.6.3

Sign up to get free protection for your applications and to get access to all the features.
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