graphql 1.10.0 → 1.10.1
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 +8 -0
- data/lib/graphql/analysis/ast/query_complexity.rb +163 -73
- data/lib/graphql/define/defined_object_proxy.rb +5 -8
- data/lib/graphql/define/instance_definable.rb +8 -2
- data/lib/graphql/execution/lookahead.rb +22 -12
- data/lib/graphql/pagination/relation_connection.rb +54 -29
- data/lib/graphql/query/variables.rb +0 -1
- data/lib/graphql/rake_task/validate.rb +3 -0
- data/lib/graphql/schema.rb +5 -0
- data/lib/graphql/schema/directive.rb +6 -1
- data/lib/graphql/schema/directive/include.rb +1 -1
- data/lib/graphql/schema/directive/skip.rb +1 -1
- data/lib/graphql/schema/field.rb +1 -1
- data/lib/graphql/schema/input_object.rb +4 -0
- data/lib/graphql/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 206501adb93cafefa37313d5cb3bc58413701cd5a2cc4028a9b749c119d7868d
|
4
|
+
data.tar.gz: 34e9b0f601cb1517c1060b4c964e6a25e020540b115d876af8c3c775cfd28793
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e68296c1cb93a1164ce0123a77fc0ec7efd86e51afd029b808bb3f466b2287744d1e54b01f685b1bcf71c7e4265a064b1e88ee5cfbcfc6647312758d2e7962f9
|
7
|
+
data.tar.gz: 01de56922522f3984950ddfc1821c7d15fd27ea7055470cf6102f9af9fa3c8402eefa41061c82d83a9461d4ae9df3f45935e10775ac6322ef9e0c40bd978c3a1
|
data/lib/graphql.rb
CHANGED
@@ -7,6 +7,14 @@ require "forwardable"
|
|
7
7
|
require_relative "./graphql/railtie" if defined? Rails::Railtie
|
8
8
|
|
9
9
|
module GraphQL
|
10
|
+
# forwards-compat for argument handling
|
11
|
+
module Ruby2Keywords
|
12
|
+
if RUBY_VERSION < "2.7"
|
13
|
+
def ruby2_keywords(*)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
10
18
|
class Error < StandardError
|
11
19
|
end
|
12
20
|
|
@@ -8,7 +8,7 @@ module GraphQL
|
|
8
8
|
# - `complexities_on_type` holds complexity scores for each type in an IRep node
|
9
9
|
def initialize(query)
|
10
10
|
super
|
11
|
-
@
|
11
|
+
@complexities_on_type_by_query = {}
|
12
12
|
end
|
13
13
|
|
14
14
|
# Overide this method to use the complexity result
|
@@ -16,17 +16,68 @@ module GraphQL
|
|
16
16
|
max_possible_complexity
|
17
17
|
end
|
18
18
|
|
19
|
+
class ScopedTypeComplexity
|
20
|
+
# A single proc for {#scoped_children} hashes. Use this to avoid repeated allocations,
|
21
|
+
# since the lexical binding isn't important.
|
22
|
+
HASH_CHILDREN = ->(h, k) { h[k] = {} }
|
23
|
+
|
24
|
+
# @param node [Language::Nodes::Field] The AST node; used for providing argument values when necessary
|
25
|
+
# @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration
|
26
|
+
# @param query [GrpahQL::Query] Used for `query.possible_types`
|
27
|
+
def initialize(node, field_definition, query)
|
28
|
+
@field_definition = field_definition
|
29
|
+
@query = query
|
30
|
+
@node = node
|
31
|
+
@scoped_children = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns true if this field has no selections, ie, it's a scalar.
|
35
|
+
# We need a quick way to check whether we should continue traversing.
|
36
|
+
def terminal?
|
37
|
+
@scoped_children.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
# This value is only calculated when asked for to avoid needless hash allocations.
|
41
|
+
# Also, if it's never asked for, we determine that this scope complexity
|
42
|
+
# is a scalar field ({#terminal?}).
|
43
|
+
# @return [Hash<Hash<Class => ScopedTypeComplexity>]
|
44
|
+
def scoped_children
|
45
|
+
@scoped_children ||= Hash.new(&HASH_CHILDREN)
|
46
|
+
end
|
47
|
+
|
48
|
+
def own_complexity(child_complexity)
|
49
|
+
defined_complexity = @field_definition.complexity
|
50
|
+
case defined_complexity
|
51
|
+
when Proc
|
52
|
+
arguments = @query.arguments_for(@node, @field_definition)
|
53
|
+
defined_complexity.call(@query.context, arguments, child_complexity)
|
54
|
+
when Numeric
|
55
|
+
defined_complexity + child_complexity
|
56
|
+
else
|
57
|
+
raise("Invalid complexity: #{defined_complexity.inspect} on #{@field_definition.name}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
19
62
|
def on_enter_field(node, parent, visitor)
|
20
63
|
# We don't want to visit fragment definitions,
|
21
64
|
# we'll visit them when we hit the spreads instead
|
22
65
|
return if visitor.visiting_fragment_definition?
|
23
66
|
return if visitor.skipping?
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
67
|
+
parent_type = visitor.parent_type_definition
|
68
|
+
field_key = node.alias || node.name
|
69
|
+
# Find the complexity calculation for this field --
|
70
|
+
# if we're re-entering a selection, we'll already have one.
|
71
|
+
# Otherwise, make a new one and store it.
|
72
|
+
#
|
73
|
+
# `node` and `visitor.field_definition` may appear from a cache,
|
74
|
+
# but I think that's ok. If the arguments _didn't_ match,
|
75
|
+
# then the query would have been rejected as invalid.
|
76
|
+
complexities_on_type = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query)]
|
77
|
+
|
78
|
+
complexity = complexities_on_type.last.scoped_children[parent_type][field_key] ||= ScopedTypeComplexity.new(node, visitor.field_definition, visitor.query)
|
79
|
+
# Push it on the stack.
|
80
|
+
complexities_on_type.push(complexity)
|
30
81
|
end
|
31
82
|
|
32
83
|
def on_leave_field(node, parent, visitor)
|
@@ -34,87 +85,126 @@ module GraphQL
|
|
34
85
|
# we'll visit them when we hit the spreads instead
|
35
86
|
return if visitor.visiting_fragment_definition?
|
36
87
|
return if visitor.skipping?
|
37
|
-
|
38
|
-
|
39
|
-
child_complexity = type_complexities.max_possible_complexity
|
40
|
-
own_complexity = get_complexity(node, visitor.field_definition, child_complexity, visitor)
|
41
|
-
|
42
|
-
if @complexities_on_type.last.is_a?(AbstractTypeComplexity)
|
43
|
-
key = selection_key(visitor.response_path, visitor.query)
|
44
|
-
parent_type = visitor.parent_type_definition
|
45
|
-
visitor.query.possible_types(parent_type).each do |type|
|
46
|
-
@complexities_on_type.last.merge(type, key, own_complexity)
|
47
|
-
end
|
48
|
-
else
|
49
|
-
@complexities_on_type.last.merge(own_complexity)
|
50
|
-
end
|
88
|
+
complexities_on_type = @complexities_on_type_by_query[visitor.query]
|
89
|
+
complexities_on_type.pop
|
51
90
|
end
|
52
91
|
|
53
92
|
# @return [Integer]
|
54
93
|
def max_possible_complexity
|
55
|
-
@complexities_on_type
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
def selection_key(response_path, query)
|
61
|
-
# We add the query object id to support multiplex queries
|
62
|
-
# even if they have the same response path, they should
|
63
|
-
# always be added.
|
64
|
-
"#{response_path.join(".")}-#{query.object_id}"
|
65
|
-
end
|
66
|
-
|
67
|
-
# Get a complexity value for a field,
|
68
|
-
# by getting the number or calling its proc
|
69
|
-
def get_complexity(ast_node, field_defn, child_complexity, visitor)
|
70
|
-
# Return if we've visited this response path before (not counting duplicates)
|
71
|
-
defined_complexity = field_defn.complexity
|
72
|
-
|
73
|
-
arguments = visitor.arguments_for(ast_node, field_defn)
|
74
|
-
|
75
|
-
case defined_complexity
|
76
|
-
when Proc
|
77
|
-
defined_complexity.call(visitor.query.context, arguments, child_complexity)
|
78
|
-
when Numeric
|
79
|
-
defined_complexity + (child_complexity || 0)
|
80
|
-
else
|
81
|
-
raise("Invalid complexity: #{defined_complexity.inspect} on #{field_defn.name}")
|
94
|
+
@complexities_on_type_by_query.reduce(0) do |total, (query, complexities_on_type)|
|
95
|
+
root_complexity = complexities_on_type.last
|
96
|
+
# Use this entry point to calculate the total complexity
|
97
|
+
total_complexity_for_query = ComplexityMergeFunctions.merged_max_complexity_for_scopes(query, [root_complexity.scoped_children])
|
98
|
+
total + total_complexity_for_query
|
82
99
|
end
|
83
100
|
end
|
84
101
|
|
85
|
-
#
|
86
|
-
#
|
87
|
-
|
88
|
-
|
89
|
-
|
102
|
+
# These functions use `ScopedTypeComplexity` objects,
|
103
|
+
# especially their `scoped_children`, to traverse down the tree
|
104
|
+
# and find the max complexity for any possible runtime type.
|
105
|
+
# Yowza.
|
106
|
+
module ComplexityMergeFunctions
|
107
|
+
module_function
|
108
|
+
# When looking at two selection scopes, figure out whether the selections on
|
109
|
+
# `right_scope` should be applied when analyzing `left_scope`.
|
110
|
+
# This is like the `Typecast.subtype?`, except it's using query-specific type filtering.
|
111
|
+
def applies_to?(query, left_scope, right_scope)
|
112
|
+
if left_scope == right_scope
|
113
|
+
# This can happen when several branches are being analyzed together
|
114
|
+
true
|
115
|
+
else
|
116
|
+
# Check if these two scopes have _any_ types in common.
|
117
|
+
possible_right_types = query.possible_types(right_scope)
|
118
|
+
possible_left_types = query.possible_types(left_scope)
|
119
|
+
!(possible_right_types & possible_left_types).empty?
|
120
|
+
end
|
90
121
|
end
|
91
122
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
123
|
+
def merged_max_complexity_for_scopes(query, scoped_children_hashes)
|
124
|
+
# Figure out what scopes are possible here.
|
125
|
+
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
126
|
+
all_scopes = {}
|
127
|
+
scoped_children_hashes.each do |h|
|
128
|
+
all_scopes.merge!(h)
|
98
129
|
end
|
99
|
-
max
|
100
|
-
end
|
101
130
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
131
|
+
# If an abstract scope is present, but _all_ of its concrete types
|
132
|
+
# are also in the list, remove it from the list of scopes to check,
|
133
|
+
# because every possible type is covered by a concrete type.
|
134
|
+
# (That is, there are no remainder types to check.)
|
135
|
+
all_scopes.reject! do |scope, _|
|
136
|
+
scope.kind.abstract? && (
|
137
|
+
query.possible_types(scope).all? { |t| all_scopes.include?(t) }
|
138
|
+
)
|
139
|
+
end
|
108
140
|
|
109
|
-
|
110
|
-
|
141
|
+
# This will hold `{ type => int }` pairs, one for each possible branch
|
142
|
+
complexity_by_scope = {}
|
143
|
+
|
144
|
+
# For each scope,
|
145
|
+
# find the lexical selections that might apply to it,
|
146
|
+
# and gather them together into an array.
|
147
|
+
# Then, treat the set of selection hashes
|
148
|
+
# as a set and calculate the complexity for them as a unit
|
149
|
+
all_scopes.each do |scope, _|
|
150
|
+
# These will be the selections on `scope`
|
151
|
+
children_for_scope = []
|
152
|
+
scoped_children_hashes.each do |sc_h|
|
153
|
+
sc_h.each do |inner_scope, children_hash|
|
154
|
+
if applies_to?(query, scope, inner_scope)
|
155
|
+
children_for_scope << children_hash
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Calculate the complexity for `scope`, merging all
|
161
|
+
# possible lexical branches.
|
162
|
+
complexity_value = merged_max_complexity(query, children_for_scope)
|
163
|
+
complexity_by_scope[scope] = complexity_value
|
164
|
+
end
|
111
165
|
|
112
|
-
|
113
|
-
|
166
|
+
# Return the max complexity among all scopes
|
167
|
+
complexity_by_scope.each_value.max
|
114
168
|
end
|
115
169
|
|
116
|
-
|
117
|
-
|
170
|
+
# @param children_for_scope [Array<Hash>] An array of `scoped_children[scope]` hashes (`{field_key => complexity}`)
|
171
|
+
# @return [Integer] Complexity value for all these selections in the current scope
|
172
|
+
def merged_max_complexity(query, children_for_scope)
|
173
|
+
all_keys = []
|
174
|
+
children_for_scope.each do |c|
|
175
|
+
all_keys.concat(c.keys)
|
176
|
+
end
|
177
|
+
all_keys.uniq!
|
178
|
+
complexity_for_keys = {}
|
179
|
+
all_keys.each do |child_key|
|
180
|
+
|
181
|
+
scoped_children_for_key = nil
|
182
|
+
complexity_for_key = nil
|
183
|
+
children_for_scope.each do |children_hash|
|
184
|
+
if children_hash.key?(child_key)
|
185
|
+
complexity_for_key = children_hash[child_key]
|
186
|
+
if complexity_for_key.terminal?
|
187
|
+
# Assume that all terminals would return the same complexity
|
188
|
+
# Since it's a terminal, its child complexity is zero.
|
189
|
+
complexity_for_key = complexity_for_key.own_complexity(0)
|
190
|
+
complexity_for_keys[child_key] = complexity_for_key
|
191
|
+
else
|
192
|
+
scoped_children_for_key ||= []
|
193
|
+
scoped_children_for_key << complexity_for_key.scoped_children
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
if scoped_children_for_key
|
199
|
+
child_complexity = merged_max_complexity_for_scopes(query, scoped_children_for_key)
|
200
|
+
# This is the _last_ one we visited; assume it's representative.
|
201
|
+
max_complexity = complexity_for_key.own_complexity(child_complexity)
|
202
|
+
complexity_for_keys[child_key] = max_complexity
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Calculate the child complexity by summing the complexity of all selections
|
207
|
+
complexity_for_keys.each_value.inject(0, &:+)
|
118
208
|
end
|
119
209
|
end
|
120
210
|
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module GraphQL
|
3
4
|
module Define
|
4
5
|
# This object delegates most methods to a dictionary of functions, {@dictionary}.
|
5
6
|
# {@target} is passed to the specified function, along with any arguments and block.
|
6
7
|
# This allows a method-based DSL without adding methods to the defined class.
|
7
8
|
class DefinedObjectProxy
|
9
|
+
extend GraphQL::Ruby2Keywords
|
8
10
|
# The object which will be defined by definition functions
|
9
11
|
attr_reader :target
|
10
12
|
|
@@ -32,16 +34,11 @@ module GraphQL
|
|
32
34
|
end
|
33
35
|
|
34
36
|
# Lookup a function from the dictionary and call it if it's found.
|
35
|
-
|
37
|
+
ruby2_keywords
|
38
|
+
def method_missing(name, *args, &block)
|
36
39
|
definition = @dictionary[name]
|
37
40
|
if definition
|
38
|
-
|
39
|
-
# Ruby 2.7 does fine here, but older Rubies receive too many arguments.
|
40
|
-
if kwargs.any?
|
41
|
-
definition.call(@target, *args, **kwargs, &block)
|
42
|
-
else
|
43
|
-
definition.call(@target, *args, &block)
|
44
|
-
end
|
41
|
+
definition.call(@target, *args, &block)
|
45
42
|
else
|
46
43
|
msg = "#{@target.class.name} can't define '#{name}'"
|
47
44
|
raise NoDefinitionError, msg, caller
|
@@ -191,12 +191,18 @@ module GraphQL
|
|
191
191
|
end
|
192
192
|
|
193
193
|
class AssignAttribute
|
194
|
+
extend GraphQL::Ruby2Keywords
|
195
|
+
|
194
196
|
def initialize(attr_name)
|
195
197
|
@attr_assign_method = :"#{attr_name}="
|
196
198
|
end
|
197
199
|
|
198
|
-
|
199
|
-
|
200
|
+
# Even though we're just using the first value here,
|
201
|
+
# We have to add a splat here to use `ruby2_keywords`,
|
202
|
+
# so that it will accept a `[{}]` input from the caller.
|
203
|
+
ruby2_keywords
|
204
|
+
def call(defn, *value)
|
205
|
+
defn.public_send(@attr_assign_method, value.first)
|
200
206
|
end
|
201
207
|
end
|
202
208
|
end
|
@@ -51,7 +51,7 @@ module GraphQL
|
|
51
51
|
|
52
52
|
# @return [Hash<Symbol, Object>]
|
53
53
|
def arguments
|
54
|
-
@arguments ||= @field && ArgumentHelpers.arguments(@query,
|
54
|
+
@arguments ||= @field && ArgumentHelpers.arguments(@query, @field, ast_nodes.first)
|
55
55
|
end
|
56
56
|
|
57
57
|
# True if this node has a selection on `field_name`.
|
@@ -203,8 +203,22 @@ module GraphQL
|
|
203
203
|
end
|
204
204
|
end
|
205
205
|
|
206
|
+
def skipped_by_directive?(ast_selection)
|
207
|
+
ast_selection.directives.each do |directive|
|
208
|
+
dir_defn = @query.schema.directives.fetch(directive.name)
|
209
|
+
directive_class = dir_defn.type_class
|
210
|
+
if directive_class
|
211
|
+
dir_args = GraphQL::Execution::Lookahead::ArgumentHelpers.arguments(@query, dir_defn, directive)
|
212
|
+
return true unless directive_class.static_include?(dir_args, @query.context)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
false
|
216
|
+
end
|
217
|
+
|
206
218
|
def find_selections(subselections_by_type, selections_on_type, selected_type, ast_selections, arguments)
|
207
219
|
ast_selections.each do |ast_selection|
|
220
|
+
next if skipped_by_directive?(ast_selection)
|
221
|
+
|
208
222
|
case ast_selection
|
209
223
|
when GraphQL::Language::Nodes::Field
|
210
224
|
response_key = ast_selection.alias || ast_selection.name
|
@@ -242,6 +256,7 @@ module GraphQL
|
|
242
256
|
# If a selection on `node` matches `field_name` (which is backed by `field_defn`)
|
243
257
|
# and matches the `arguments:` constraints, then add that node to `matches`
|
244
258
|
def find_selected_nodes(node, field_name, field_defn, arguments:, matches:)
|
259
|
+
return if skipped_by_directive?(node)
|
245
260
|
case node
|
246
261
|
when GraphQL::Language::Nodes::Field
|
247
262
|
if node.name == field_name
|
@@ -263,7 +278,7 @@ module GraphQL
|
|
263
278
|
end
|
264
279
|
|
265
280
|
def arguments_match?(arguments, field_defn, field_node)
|
266
|
-
query_kwargs = ArgumentHelpers.arguments(@query,
|
281
|
+
query_kwargs = ArgumentHelpers.arguments(@query, field_defn, field_node)
|
267
282
|
arguments.all? do |arg_name, arg_value|
|
268
283
|
arg_name = normalize_keyword(arg_name)
|
269
284
|
# Make sure the constraint is present with a matching value
|
@@ -275,20 +290,15 @@ module GraphQL
|
|
275
290
|
module ArgumentHelpers
|
276
291
|
module_function
|
277
292
|
|
278
|
-
def arguments(query,
|
293
|
+
def arguments(query, arg_owner, ast_node)
|
279
294
|
kwarg_arguments = {}
|
280
295
|
arg_defns = arg_owner.arguments
|
281
296
|
ast_node.arguments.each do |arg|
|
282
297
|
arg_defn = arg_defns[arg.name] || raise("Invariant: missing argument definition for #{arg.name.inspect} in #{arg_defns.keys} from #{arg_owner}")
|
283
298
|
# Need to distinguish between client-provided `nil`
|
284
299
|
# and nothing-at-all
|
285
|
-
is_present, value = arg_to_value(query,
|
300
|
+
is_present, value = arg_to_value(query, arg_defn.type, arg.value)
|
286
301
|
if is_present
|
287
|
-
# This doesn't apply to directives, which are legacy
|
288
|
-
# Can remove this when Skip and Include use classes or something.
|
289
|
-
if graphql_object
|
290
|
-
value = arg_defn.prepare_value(graphql_object, value)
|
291
|
-
end
|
292
302
|
kwarg_arguments[arg_defn.keyword] = value
|
293
303
|
end
|
294
304
|
end
|
@@ -305,7 +315,7 @@ module GraphQL
|
|
305
315
|
# @param arg_type [Class, GraphQL::Schema::NonNull, GraphQL::Schema::List]
|
306
316
|
# @param ast_value [GraphQL::Language::Nodes::VariableIdentifier, String, Integer, Float, Boolean]
|
307
317
|
# @return [Array(is_present, value)]
|
308
|
-
def arg_to_value(query,
|
318
|
+
def arg_to_value(query, arg_type, ast_value)
|
309
319
|
if ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
310
320
|
# If it's not here, it will get added later
|
311
321
|
if query.variables.key?(ast_value.name)
|
@@ -316,13 +326,13 @@ module GraphQL
|
|
316
326
|
elsif ast_value.is_a?(GraphQL::Language::Nodes::NullValue)
|
317
327
|
return true, nil
|
318
328
|
elsif arg_type.is_a?(GraphQL::Schema::NonNull)
|
319
|
-
arg_to_value(query,
|
329
|
+
arg_to_value(query, arg_type.of_type, ast_value)
|
320
330
|
elsif arg_type.is_a?(GraphQL::Schema::List)
|
321
331
|
# Treat a single value like a list
|
322
332
|
arg_value = Array(ast_value)
|
323
333
|
list = []
|
324
334
|
arg_value.map do |inner_v|
|
325
|
-
_present, value = arg_to_value(query,
|
335
|
+
_present, value = arg_to_value(query, arg_type.of_type, inner_v)
|
326
336
|
list << value
|
327
337
|
end
|
328
338
|
return true, list
|
@@ -11,12 +11,32 @@ module GraphQL
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def has_previous_page
|
14
|
-
|
14
|
+
if @has_previous_page.nil?
|
15
|
+
@has_previous_page = if @after_offset && @after_offset > 0
|
16
|
+
true
|
17
|
+
elsif last
|
18
|
+
# See whether there are any nodes _before_ the current offset.
|
19
|
+
# If there _is no_ current offset, then there can't be any nodes before it.
|
20
|
+
# Assume that if the offset is positive, there are nodes before the offset.
|
21
|
+
limited_nodes
|
22
|
+
!(@paged_nodes_offset.nil? || @paged_nodes_offset == 0)
|
23
|
+
else
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
15
27
|
@has_previous_page
|
16
28
|
end
|
17
29
|
|
18
30
|
def has_next_page
|
19
|
-
|
31
|
+
if @has_next_page.nil?
|
32
|
+
@has_next_page = if @before_offset && @before_offset > 0
|
33
|
+
true
|
34
|
+
elsif first
|
35
|
+
relation_count(set_limit(sliced_nodes, first + 1)) == first + 1
|
36
|
+
else
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
20
40
|
@has_next_page
|
21
41
|
end
|
22
42
|
|
@@ -80,37 +100,45 @@ module GraphQL
|
|
80
100
|
end
|
81
101
|
end
|
82
102
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
def
|
86
|
-
@
|
103
|
+
# Apply `before` and `after` to the underlying `items`,
|
104
|
+
# returning a new relation.
|
105
|
+
def sliced_nodes
|
106
|
+
@sliced_nodes ||= begin
|
87
107
|
paginated_nodes = items
|
88
|
-
after_offset = after && offset_from_cursor(after)
|
89
|
-
before_offset = before && offset_from_cursor(before)
|
108
|
+
@after_offset = after && offset_from_cursor(after)
|
109
|
+
@before_offset = before && offset_from_cursor(before)
|
90
110
|
|
91
|
-
if after_offset
|
111
|
+
if @after_offset
|
92
112
|
previous_offset = relation_offset(items) || 0
|
93
|
-
paginated_nodes = set_offset(paginated_nodes, previous_offset + after_offset)
|
113
|
+
paginated_nodes = set_offset(paginated_nodes, previous_offset + @after_offset)
|
94
114
|
end
|
95
115
|
|
96
|
-
if before_offset && after_offset
|
97
|
-
if after_offset < before_offset
|
116
|
+
if @before_offset && @after_offset
|
117
|
+
if @after_offset < @before_offset
|
98
118
|
# Get the number of items between the two cursors
|
99
|
-
space_between = before_offset - after_offset - 1
|
119
|
+
space_between = @before_offset - @after_offset - 1
|
100
120
|
paginated_nodes = set_limit(paginated_nodes, space_between)
|
101
121
|
else
|
102
122
|
# TODO I think this is untested
|
103
123
|
# The cursors overextend one another to an empty set
|
104
124
|
paginated_nodes = null_relation(paginated_nodes)
|
105
125
|
end
|
106
|
-
elsif before_offset
|
126
|
+
elsif @before_offset
|
107
127
|
# Use limit to cut off the tail of the relation
|
108
|
-
paginated_nodes = set_limit(paginated_nodes, before_offset - 1)
|
128
|
+
paginated_nodes = set_limit(paginated_nodes, @before_offset - 1)
|
109
129
|
end
|
110
130
|
|
111
|
-
|
131
|
+
paginated_nodes
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Apply `first` and `last` to `sliced_nodes`,
|
136
|
+
# returning a new relation
|
137
|
+
def limited_nodes
|
138
|
+
@limited_nodes ||= begin
|
139
|
+
paginated_nodes = sliced_nodes
|
112
140
|
|
113
|
-
if first && (relation_limit(paginated_nodes).nil? || relation_limit(paginated_nodes) > first)
|
141
|
+
if first && (relation_limit(paginated_nodes).nil? || relation_limit(paginated_nodes) > first) && last.nil?
|
114
142
|
# `first` would create a stricter limit that the one already applied, so add it
|
115
143
|
paginated_nodes = set_limit(paginated_nodes, first)
|
116
144
|
end
|
@@ -125,27 +153,24 @@ module GraphQL
|
|
125
153
|
end
|
126
154
|
else
|
127
155
|
# No limit, so get the last items
|
156
|
+
sliced_nodes_count = relation_count(@sliced_nodes)
|
128
157
|
offset = (relation_offset(paginated_nodes) || 0) + sliced_nodes_count - [last, sliced_nodes_count].min
|
129
158
|
paginated_nodes = set_offset(paginated_nodes, offset)
|
130
159
|
paginated_nodes = set_limit(paginated_nodes, last)
|
131
160
|
end
|
132
161
|
end
|
133
162
|
|
134
|
-
@has_next_page = !!(
|
135
|
-
(before_offset && before_offset > 0) ||
|
136
|
-
(first && sliced_nodes_count > first)
|
137
|
-
)
|
138
|
-
|
139
|
-
@has_previous_page = !!(
|
140
|
-
(after_offset && after_offset > 0) ||
|
141
|
-
(last && sliced_nodes_count > last)
|
142
|
-
)
|
143
|
-
|
144
163
|
@paged_nodes_offset = relation_offset(paginated_nodes)
|
145
|
-
|
146
|
-
paginated_nodes.to_a
|
164
|
+
paginated_nodes
|
147
165
|
end
|
148
166
|
end
|
167
|
+
|
168
|
+
# Load nodes after applying first/last/before/after,
|
169
|
+
# returns an array of nodes
|
170
|
+
def load_nodes
|
171
|
+
# Return an array so we can consistently use `.index(node)` on it
|
172
|
+
@nodes ||= limited_nodes.to_a
|
173
|
+
end
|
149
174
|
end
|
150
175
|
end
|
151
176
|
end
|
@@ -29,7 +29,6 @@ module GraphQL
|
|
29
29
|
default_value = ast_variable.default_value
|
30
30
|
provided_value = @provided_variables[variable_name]
|
31
31
|
value_was_provided = @provided_variables.key?(variable_name)
|
32
|
-
|
33
32
|
begin
|
34
33
|
validation_result = variable_type.validate_input(provided_value, ctx)
|
35
34
|
if validation_result.valid?
|
@@ -7,6 +7,9 @@ module GraphQL
|
|
7
7
|
desc "Get the checksum of a graphql-pro version and compare it to published versions on GitHub and graphql-ruby.org"
|
8
8
|
task "graphql:pro:validate", [:gem_version] do |t, args|
|
9
9
|
version = args[:gem_version]
|
10
|
+
if version.nil?
|
11
|
+
raise ArgumentError, "A specific version is required, eg `rake graphql:pro:validate[1.12.0]`"
|
12
|
+
end
|
10
13
|
check = "\e[32m✓\e[0m"
|
11
14
|
ex = "\e[31m✘\e[0m"
|
12
15
|
puts "Validating graphql-pro v#{version}"
|
data/lib/graphql/schema.rb
CHANGED
@@ -1817,6 +1817,11 @@ module GraphQL
|
|
1817
1817
|
add_type(t, owner: type, late_types: late_types)
|
1818
1818
|
end
|
1819
1819
|
end
|
1820
|
+
if type.kind.interface?
|
1821
|
+
type.orphan_types.each do |t|
|
1822
|
+
add_type(t, owner: type, late_types: late_types)
|
1823
|
+
end
|
1824
|
+
end
|
1820
1825
|
if type.kind.object?
|
1821
1826
|
own_possible_types[type.graphql_name] = [type]
|
1822
1827
|
type.interfaces.each do |i|
|
@@ -60,7 +60,12 @@ module GraphQL
|
|
60
60
|
end
|
61
61
|
|
62
62
|
# If false, this part of the query won't be evaluated
|
63
|
-
def include?(_object,
|
63
|
+
def include?(_object, arguments, context)
|
64
|
+
static_include?(arguments, context)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Determines whether {Execution::Lookahead} considers the field to be selected
|
68
|
+
def static_include?(_arguments, _context)
|
64
69
|
true
|
65
70
|
end
|
66
71
|
|
data/lib/graphql/schema/field.rb
CHANGED
@@ -689,7 +689,7 @@ module GraphQL
|
|
689
689
|
# Written iteratively to avoid big stack traces.
|
690
690
|
# @return [Object] Whatever the
|
691
691
|
def with_extensions(obj, args, ctx)
|
692
|
-
if @extensions.
|
692
|
+
if @extensions.empty?
|
693
693
|
yield(obj, args)
|
694
694
|
else
|
695
695
|
# Save these so that the originals can be re-given to `after_resolve` handlers.
|
@@ -185,11 +185,15 @@ module GraphQL
|
|
185
185
|
end
|
186
186
|
|
187
187
|
def coerce_input(value, ctx)
|
188
|
+
if value.nil?
|
189
|
+
return nil
|
190
|
+
end
|
188
191
|
input_values = {}
|
189
192
|
|
190
193
|
arguments.each do |name, argument_defn|
|
191
194
|
arg_key = argument_defn.keyword
|
192
195
|
has_value = false
|
196
|
+
|
193
197
|
# Accept either string or symbol
|
194
198
|
field_value = if value.key?(name)
|
195
199
|
has_value = true
|
data/lib/graphql/version.rb
CHANGED
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.10.
|
4
|
+
version: 1.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-01-
|
11
|
+
date: 2020-01-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: benchmark-ips
|