graphql 1.6.2 → 1.6.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql.rb +18 -1
- data/lib/graphql/analysis/query_complexity.rb +1 -1
- data/lib/graphql/backwards_compatibility.rb +1 -1
- data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -2
- data/lib/graphql/execution/execute.rb +12 -3
- data/lib/graphql/execution/lazy.rb +7 -2
- data/lib/graphql/execution/lazy/lazy_method_map.rb +1 -1
- data/lib/graphql/execution/lazy/resolve.rb +38 -15
- data/lib/graphql/execution/multiplex.rb +48 -35
- data/lib/graphql/field.rb +11 -1
- data/lib/graphql/internal_representation/node.rb +3 -1
- data/lib/graphql/non_null_type.rb +1 -1
- data/lib/graphql/query.rb +1 -1
- data/lib/graphql/query/arguments.rb +5 -3
- data/lib/graphql/query/arguments_cache.rb +10 -6
- data/lib/graphql/query/context.rb +18 -8
- data/lib/graphql/query/null_context.rb +1 -1
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/relay.rb +1 -0
- data/lib/graphql/relay/base_connection.rb +7 -9
- data/lib/graphql/relay/connection_type.rb +6 -15
- data/lib/graphql/relay/edges_instrumentation.rb +56 -0
- data/lib/graphql/schema.rb +2 -1
- data/lib/graphql/schema/middleware_chain.rb +2 -11
- data/lib/graphql/schema/type_map.rb +1 -1
- data/lib/graphql/static_validation/definition_dependencies.rb +1 -1
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/execution/multiplex_spec.rb +4 -0
- data/spec/graphql/query_spec.rb +24 -1
- data/spec/support/dummy/data.rb +7 -1
- data/spec/support/star_wars/schema.rb +18 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8eca19d9b215aed67ef5434bb5dab5cce3e8626
|
4
|
+
data.tar.gz: 66740ce037cbd8e4019ed7516503e430ebb0a34a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e34b8bce773c1304d9c4796a20a92c731de182ad1a144df2bb4a814295a2233b298d8817d24c96837540b57fd73ca3a9acb55b286308c1b7a3b3725f31c3e11
|
7
|
+
data.tar.gz: b9cce7a863dfaabcb7312a0844ef659504cf5adedfe7b22246ce4856ae78d71c6204535a1351b5d0c746161b5155d06cd25f845a0d8fdc3776a9ee27ed41160d
|
data/lib/graphql.rb
CHANGED
@@ -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
|
72
|
+
@types = Hash.new(0)
|
73
73
|
end
|
74
74
|
|
75
75
|
# Return the max possible complexity for types in this selection
|
@@ -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
|
-
|
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
|
-
|
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
|
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
|
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
|
43
|
+
def then
|
44
44
|
self.class.new {
|
45
|
-
|
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
|
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
|
-
|
33
|
+
acc = each_lazy(NullAccumulator, value)
|
17
34
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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,
|
30
|
-
#
|
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(
|
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(
|
57
|
+
acc = each_lazy(acc, field_result)
|
37
58
|
end
|
38
59
|
when Array
|
39
60
|
value.each do |field_result|
|
40
|
-
each_lazy(
|
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
|
-
|
66
|
+
acc = acc << value
|
46
67
|
else
|
47
|
-
each_lazy(
|
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
|
50
|
-
|
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
|
-
|
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(
|
64
|
-
|
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
|
-
|
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
|
data/lib/graphql/field.rb
CHANGED
@@ -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
|
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)) }
|
data/lib/graphql/query.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
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
|
-
|
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
|
91
|
+
extend GraphQL::Delegate
|
91
92
|
|
92
|
-
attr_reader :
|
93
|
+
attr_reader :selection, :field, :parent_type, :query, :schema
|
93
94
|
|
94
|
-
def initialize(context:,
|
95
|
+
def initialize(context:, key:, selection:, parent:, field:, parent_type:)
|
95
96
|
@context = context
|
96
|
-
@
|
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, :
|
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
|
-
|
143
|
+
parent: self,
|
144
|
+
key: key,
|
135
145
|
selection: selection,
|
136
146
|
parent_type: parent_type,
|
137
147
|
field: field,
|
@@ -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
|
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
|
data/lib/graphql/relay.rb
CHANGED
@@ -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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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:
|
13
|
-
|
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]
|
23
|
-
|
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]
|
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
|
data/lib/graphql/schema.rb
CHANGED
@@ -98,7 +98,7 @@ module GraphQL
|
|
98
98
|
GraphQL::Filter.new(except: default_mask)
|
99
99
|
end
|
100
100
|
|
101
|
-
self.default_execution_strategy =
|
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
|
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
|
11
|
+
extend GraphQL::Delegate
|
12
12
|
def_delegators :@storage, :key?, :keys, :values, :to_h, :fetch, :each, :each_value
|
13
13
|
|
14
14
|
def initialize
|
@@ -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
|
15
|
+
extend GraphQL::Delegate
|
16
16
|
|
17
17
|
attr_reader :query, :schema,
|
18
18
|
:document, :errors, :visitor,
|
data/lib/graphql/version.rb
CHANGED
data/spec/graphql/query_spec.rb
CHANGED
@@ -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) {
|
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
|
data/spec/support/dummy/data.rb
CHANGED
@@ -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
|
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.
|
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-
|
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
|