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.
- 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
|