graphql 2.6.3 → 2.6.10
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/generators/graphql/field_extractor.rb +17 -1
- data/lib/generators/graphql/templates/schema.erb +2 -1
- data/lib/graphql/analysis/query_complexity.rb +2 -2
- data/lib/graphql/dashboard/application_controller.rb +5 -1
- data/lib/graphql/dashboard.rb +3 -0
- data/lib/graphql/dataloader/async_dataloader.rb +333 -68
- data/lib/graphql/dataloader/source.rb +32 -19
- data/lib/graphql/dataloader.rb +52 -36
- data/lib/graphql/execution/field_resolve_step.rb +79 -19
- data/lib/graphql/execution/finalize.rb +7 -8
- data/lib/graphql/execution/interpreter/runtime.rb +1 -8
- data/lib/graphql/execution/interpreter.rb +9 -1
- data/lib/graphql/execution/prepare_object_step.rb +8 -4
- data/lib/graphql/execution/runner.rb +17 -5
- data/lib/graphql/execution/selections_step.rb +10 -6
- data/lib/graphql/execution_error.rb +4 -0
- data/lib/graphql/float_decoding_error.rb +13 -0
- data/lib/graphql/float_encoding_error.rb +28 -0
- data/lib/graphql/language/block_string.rb +6 -11
- data/lib/graphql/language/cache.rb +84 -14
- data/lib/graphql/language/lexer.rb +15 -9
- data/lib/graphql/language/nodes.rb +2 -1
- data/lib/graphql/language.rb +3 -3
- data/lib/graphql/pagination/relation_connection.rb +10 -1
- data/lib/graphql/query/variable_validation_error.rb +16 -1
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/railtie.rb +2 -1
- data/lib/graphql/schema/build_from_definition.rb +11 -3
- data/lib/graphql/schema/directive.rb +3 -0
- data/lib/graphql/schema/input_object.rb +2 -2
- data/lib/graphql/schema/resolver.rb +1 -1
- data/lib/graphql/schema/wrapper.rb +4 -0
- data/lib/graphql/schema.rb +5 -5
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +66 -29
- data/lib/graphql/subscriptions/serialize.rb +10 -4
- data/lib/graphql/types/float.rb +18 -4
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +2 -0
- metadata +4 -58
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
# This error is raised when `Types::Float` is given a non-finite input value.
|
|
4
|
+
class FloatDecodingError < GraphQL::RuntimeTypeError
|
|
5
|
+
# The value which couldn't be decoded
|
|
6
|
+
attr_reader :float_value
|
|
7
|
+
|
|
8
|
+
def initialize(value)
|
|
9
|
+
@float_value = value
|
|
10
|
+
super("Float is not finite: #{value.inspect}.")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module GraphQL
|
|
3
|
+
# This error is raised when `Types::Float` is asked to return a non-finite value.
|
|
4
|
+
class FloatEncodingError < GraphQL::RuntimeTypeError
|
|
5
|
+
# The value which couldn't be encoded
|
|
6
|
+
attr_reader :float_value
|
|
7
|
+
|
|
8
|
+
# @return [GraphQL::Schema::Field] The field that returned a non-finite float
|
|
9
|
+
attr_reader :field
|
|
10
|
+
|
|
11
|
+
# @return [Array<String, Integer>] Where the field appeared in the GraphQL response
|
|
12
|
+
attr_reader :path
|
|
13
|
+
|
|
14
|
+
def initialize(value, context:)
|
|
15
|
+
@float_value = value
|
|
16
|
+
@field = context[:current_field]
|
|
17
|
+
@path = context[:current_path]
|
|
18
|
+
message = "Float is not finite: #{value.inspect}".dup
|
|
19
|
+
if @path
|
|
20
|
+
message << " @ #{@path.join(".")}"
|
|
21
|
+
end
|
|
22
|
+
if @field
|
|
23
|
+
message << " (#{@field.path})"
|
|
24
|
+
end
|
|
25
|
+
super("#{message}.")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -21,17 +21,12 @@ module GraphQL
|
|
|
21
21
|
next
|
|
22
22
|
end
|
|
23
23
|
line_length = line.size
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
4
|
|
28
|
-
elsif line.match?(/\A[^ ]/)
|
|
29
|
-
0
|
|
30
|
-
else
|
|
31
|
-
line[/\A */].size
|
|
24
|
+
leading = 0
|
|
25
|
+
while leading < line_length && line.getbyte(leading) == 32
|
|
26
|
+
leading += 1
|
|
32
27
|
end
|
|
33
|
-
if
|
|
34
|
-
common_indent =
|
|
28
|
+
if leading < line_length && (common_indent.nil? || leading < common_indent)
|
|
29
|
+
common_indent = leading
|
|
35
30
|
end
|
|
36
31
|
end
|
|
37
32
|
|
|
@@ -41,7 +36,7 @@ module GraphQL
|
|
|
41
36
|
if idx == 0
|
|
42
37
|
next
|
|
43
38
|
else
|
|
44
|
-
line
|
|
39
|
+
line[0, common_indent] = ""
|
|
45
40
|
end
|
|
46
41
|
end
|
|
47
42
|
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require 'graphql/version'
|
|
4
4
|
require 'digest/sha2'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
require 'tempfile'
|
|
5
8
|
|
|
6
9
|
module GraphQL
|
|
7
10
|
module Language
|
|
@@ -9,7 +12,11 @@ module GraphQL
|
|
|
9
12
|
#
|
|
10
13
|
# With Rails, parser caching may enabled by setting `config.graphql.parser_cache = true` in your Rails application.
|
|
11
14
|
#
|
|
12
|
-
# The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new("some_dir")`.
|
|
15
|
+
# The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new(Pathname.new("some_dir"), secret: ENV.fetch("GRAPHQL_CACHE_SECRET"))`.
|
|
16
|
+
# The `secret` should be a stable value stored outside of the cache directory.
|
|
17
|
+
# When it isn't provided, a process-local secret is generated and cache entries
|
|
18
|
+
# are rebuilt after the process restarts. Pass `secret: nil` to disable cache
|
|
19
|
+
# signing. This should only be used when the cache directory is trusted.
|
|
13
20
|
# This will create a directory (`tmp/cache/graphql` by default) that stores a cache of parsed files.
|
|
14
21
|
#
|
|
15
22
|
# Much like [bootsnap](https://github.com/Shopify/bootsnap), the parser cache needs to be cleaned up manually.
|
|
@@ -18,32 +25,95 @@ module GraphQL
|
|
|
18
25
|
#
|
|
19
26
|
# @see GraphQL::Railtie for simple Rails integration
|
|
20
27
|
class Cache
|
|
21
|
-
|
|
28
|
+
# @param path [Pathname] The directory where cache entries are stored.
|
|
29
|
+
# @param secret [String, nil] A stable secret for verifying cache entries. When omitted,
|
|
30
|
+
# a process-local secret is generated. Pass `nil` to disable cache signing.
|
|
31
|
+
def initialize(path, secret: SecureRandom.random_bytes(32))
|
|
22
32
|
@path = path
|
|
33
|
+
@secret = secret
|
|
23
34
|
end
|
|
24
35
|
|
|
25
36
|
DIGEST = Digest::SHA256.new << GraphQL::VERSION
|
|
37
|
+
HMAC_SIZE = OpenSSL::Digest::SHA256.new.digest_length
|
|
38
|
+
InvalidCache = Class.new(StandardError)
|
|
39
|
+
private_constant :InvalidCache
|
|
26
40
|
|
|
27
41
|
def fetch(filename)
|
|
28
|
-
|
|
42
|
+
cache_key = cache_key_for(filename)
|
|
43
|
+
return yield unless cache_key
|
|
44
|
+
|
|
45
|
+
cache_path = @path.join(cache_key)
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
return load_cache(cache_path, cache_key) if cache_path.file?
|
|
49
|
+
rescue InvalidCache, SystemCallError
|
|
50
|
+
# Rebuild caches created by older versions or with an invalid signature.
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
payload = yield
|
|
29
54
|
begin
|
|
30
|
-
|
|
55
|
+
write_cache(cache_path, cache_key, payload)
|
|
31
56
|
rescue SystemCallError
|
|
32
|
-
return
|
|
57
|
+
# Parser caching is best-effort; return the parsed payload if the cache cannot be written.
|
|
33
58
|
end
|
|
34
|
-
|
|
59
|
+
payload
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def cache_key_for(filename)
|
|
65
|
+
content_digest = Digest::SHA256.file(filename).hexdigest
|
|
66
|
+
(DIGEST.dup << filename << content_digest).to_s
|
|
67
|
+
rescue SystemCallError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def load_cache(cache_path, cache_key)
|
|
72
|
+
cache_data = cache_path.binread
|
|
73
|
+
return Marshal.load(cache_data) unless @secret
|
|
74
|
+
|
|
75
|
+
signature = cache_data.byteslice(0, HMAC_SIZE)
|
|
76
|
+
payload = cache_data.byteslice(HMAC_SIZE..-1)
|
|
77
|
+
raise InvalidCache unless signature && payload
|
|
35
78
|
|
|
36
|
-
|
|
37
|
-
|
|
79
|
+
expected_signature = signature_for(cache_key, payload)
|
|
80
|
+
unless secure_compare(signature, expected_signature)
|
|
81
|
+
raise InvalidCache
|
|
82
|
+
end
|
|
83
|
+
Marshal.load(payload)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def write_cache(cache_path, cache_key, payload)
|
|
87
|
+
@path.mkpath
|
|
88
|
+
serialized_payload = Marshal.dump(payload)
|
|
89
|
+
cache_data = if @secret
|
|
90
|
+
signature_for(cache_key, serialized_payload) + serialized_payload
|
|
38
91
|
else
|
|
39
|
-
|
|
40
|
-
|
|
92
|
+
serialized_payload
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Tempfile.create(['graphql-cache-', '.tmp'], @path.to_s) do |tempfile|
|
|
96
|
+
tempfile.binmode
|
|
97
|
+
tempfile.write(cache_data)
|
|
98
|
+
tempfile.flush
|
|
99
|
+
tempfile.fsync
|
|
100
|
+
tempfile.close
|
|
101
|
+
File.rename(tempfile.path, cache_path.to_s)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def signature_for(cache_key, payload)
|
|
106
|
+
OpenSSL::HMAC.digest('SHA256', @secret, cache_key + payload)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def secure_compare(left, right)
|
|
110
|
+
return false unless left.bytesize == right.bytesize
|
|
41
111
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
payload
|
|
112
|
+
result = 0
|
|
113
|
+
left.bytes.each_with_index do |byte, index|
|
|
114
|
+
result |= byte ^ right.getbyte(index)
|
|
46
115
|
end
|
|
116
|
+
result.zero?
|
|
47
117
|
end
|
|
48
118
|
end
|
|
49
119
|
end
|
|
@@ -9,6 +9,9 @@ module GraphQL
|
|
|
9
9
|
end
|
|
10
10
|
@string = graphql_str
|
|
11
11
|
@filename = filename
|
|
12
|
+
if !@string.valid_encoding?
|
|
13
|
+
raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil)
|
|
14
|
+
end
|
|
12
15
|
@scanner = StringScanner.new(graphql_str)
|
|
13
16
|
@pos = nil
|
|
14
17
|
@max_tokens = max_tokens || Float::INFINITY
|
|
@@ -110,10 +113,6 @@ module GraphQL
|
|
|
110
113
|
@scanner.pos += 1
|
|
111
114
|
:UNKNOWN_CHAR
|
|
112
115
|
end
|
|
113
|
-
rescue ArgumentError => err
|
|
114
|
-
if err.message == "invalid byte sequence in UTF-8"
|
|
115
|
-
raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil)
|
|
116
|
-
end
|
|
117
116
|
end
|
|
118
117
|
|
|
119
118
|
def token_value
|
|
@@ -147,7 +146,7 @@ module GraphQL
|
|
|
147
146
|
"\\r" => "\r",
|
|
148
147
|
"\\t" => "\t",
|
|
149
148
|
}
|
|
150
|
-
UTF_8 = /\\u(?:([\
|
|
149
|
+
UTF_8 = /\\u(?:([\da-f]{4})|\{([\da-f]+)\})(?:\\u([\da-f]{4}))?/i
|
|
151
150
|
VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o
|
|
152
151
|
ESCAPED = /(?:#{ESCAPES}|#{UTF_8})/o
|
|
153
152
|
|
|
@@ -163,7 +162,11 @@ module GraphQL
|
|
|
163
162
|
if !str.valid_encoding? || !str.match?(VALID_STRING)
|
|
164
163
|
raise_parse_error("Bad unicode escape in #{str.inspect}")
|
|
165
164
|
else
|
|
166
|
-
|
|
165
|
+
begin
|
|
166
|
+
Lexer.replace_escaped_characters_in_place(str)
|
|
167
|
+
rescue RangeError
|
|
168
|
+
raise_parse_error("Bad unicode escape in #{str.inspect}")
|
|
169
|
+
end
|
|
167
170
|
|
|
168
171
|
if !str.valid_encoding?
|
|
169
172
|
raise_parse_error("Bad unicode escape in #{str.inspect}")
|
|
@@ -175,11 +178,14 @@ module GraphQL
|
|
|
175
178
|
end
|
|
176
179
|
|
|
177
180
|
def line_number
|
|
178
|
-
@scanner.string
|
|
181
|
+
@scanner.string.byteslice(0, @pos).b.count("\n".b) + 1
|
|
179
182
|
end
|
|
180
183
|
|
|
181
184
|
def column_number
|
|
182
|
-
@scanner.string
|
|
185
|
+
line_prefix = @scanner.string.byteslice(0, @pos)
|
|
186
|
+
line_prefix = line_prefix.b unless line_prefix.valid_encoding?
|
|
187
|
+
newline_index = line_prefix.rindex("\n")
|
|
188
|
+
newline_index ? line_prefix.length - newline_index : line_prefix.length + 1
|
|
183
189
|
end
|
|
184
190
|
|
|
185
191
|
def raise_parse_error(message, line = line_number, col = column_number)
|
|
@@ -286,7 +292,7 @@ module GraphQL
|
|
|
286
292
|
QUOTE = '"'
|
|
287
293
|
UNICODE_DIGIT = /[0-9A-Za-z]/
|
|
288
294
|
FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/
|
|
289
|
-
N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}{
|
|
295
|
+
N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}+#{Punctuation::RCURLY}}x
|
|
290
296
|
UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})}
|
|
291
297
|
STRING_ESCAPE = %r{[\\][\\/bfnrt]}
|
|
292
298
|
BLOCK_QUOTE = '"""'
|
|
@@ -304,7 +304,8 @@ module GraphQL
|
|
|
304
304
|
children_method_names.map { |m| "#{m}: NO_CHILDREN" } +
|
|
305
305
|
DEFAULT_INITIALIZE_OPTIONS
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
# Intern descriptions so identical strings across SDL documents share one frozen object.
|
|
308
|
+
assignments = scalar_method_names.map { |m| m == :description ? "@#{m} = #{m} && -#{m}" : "@#{m} = #{m}" } +
|
|
308
309
|
children_method_names.map { |m| "@#{m} = #{m}.freeze" }
|
|
309
310
|
|
|
310
311
|
if name.end_with?("Definition") && name != "FragmentDefinition"
|
data/lib/graphql/language.rb
CHANGED
|
@@ -31,11 +31,11 @@ module GraphQL
|
|
|
31
31
|
|
|
32
32
|
"[#{serialized_array}]"
|
|
33
33
|
else
|
|
34
|
-
JSON.generate(value
|
|
34
|
+
JSON.generate(value)
|
|
35
35
|
end
|
|
36
36
|
rescue JSON::GeneratorError
|
|
37
|
-
if Float
|
|
38
|
-
|
|
37
|
+
if value.is_a?(Float) && !value.finite?
|
|
38
|
+
value.to_s
|
|
39
39
|
else
|
|
40
40
|
raise
|
|
41
41
|
end
|
|
@@ -221,7 +221,16 @@ module GraphQL
|
|
|
221
221
|
# returns an array of nodes
|
|
222
222
|
def load_nodes
|
|
223
223
|
# Return an array so we can consistently use `.index(node)` on it
|
|
224
|
-
@nodes
|
|
224
|
+
return @nodes if @nodes
|
|
225
|
+
if (@context[:dataloader].is_a?(GraphQL::Dataloader::AsyncDataloader))
|
|
226
|
+
# `AsyncDataloader` may resolve sibling fields (eg, `edges` and `pageInfo`)
|
|
227
|
+
# in separate Fibers, so several callers can get here before `@nodes` is set.
|
|
228
|
+
(@load_lock ||= Mutex.new).synchronize do
|
|
229
|
+
@nodes ||= limited_nodes.to_a
|
|
230
|
+
end
|
|
231
|
+
else
|
|
232
|
+
@nodes = limited_nodes.to_a
|
|
233
|
+
end
|
|
225
234
|
end
|
|
226
235
|
end
|
|
227
236
|
end
|
|
@@ -22,7 +22,7 @@ module GraphQL
|
|
|
22
22
|
# It is possible there are other extension items in this error, so handle
|
|
23
23
|
# a one level deep merge explicitly. However beyond that only show the
|
|
24
24
|
# latest value and problems.
|
|
25
|
-
super.merge({ "extensions" => { "value" =>
|
|
25
|
+
super.merge({ "extensions" => { "value" => value_for_extensions, "problems" => validation_result.problems }}) do |key, oldValue, newValue|
|
|
26
26
|
if oldValue.respond_to?(:merge)
|
|
27
27
|
oldValue.merge(newValue)
|
|
28
28
|
else
|
|
@@ -33,6 +33,21 @@ module GraphQL
|
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
+
def value_for_extensions(value = @value)
|
|
37
|
+
case value
|
|
38
|
+
when Array
|
|
39
|
+
value.map { |item| value_for_extensions(item) }
|
|
40
|
+
when Hash
|
|
41
|
+
value.each_with_object({}) do |(key, item), result|
|
|
42
|
+
result[key] = value_for_extensions(item)
|
|
43
|
+
end
|
|
44
|
+
when Float
|
|
45
|
+
value.finite? ? value : value.to_s
|
|
46
|
+
else
|
|
47
|
+
value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
36
51
|
def problem_fields
|
|
37
52
|
@problem_fields ||= @validation_result
|
|
38
53
|
.problems
|
|
@@ -19,7 +19,7 @@ module GraphQL
|
|
|
19
19
|
@storage = ast_variables.each_with_object({}) do |ast_variable, memo|
|
|
20
20
|
if schema.validate_max_errors && schema.validate_max_errors <= @errors.count
|
|
21
21
|
add_max_errors_reached_message
|
|
22
|
-
break
|
|
22
|
+
break memo
|
|
23
23
|
end
|
|
24
24
|
# Find the right value for this variable:
|
|
25
25
|
# - First, use the value provided at runtime
|
data/lib/graphql/railtie.rb
CHANGED
|
@@ -15,7 +15,8 @@ module GraphQL
|
|
|
15
15
|
initializer("graphql.cache") do |app|
|
|
16
16
|
if config.graphql.parser_cache
|
|
17
17
|
Language::Parser.cache ||= Language::Cache.new(
|
|
18
|
-
app.root.join("tmp/cache/graphql")
|
|
18
|
+
app.root.join("tmp/cache/graphql"),
|
|
19
|
+
secret: app.secret_key_base
|
|
19
20
|
)
|
|
20
21
|
end
|
|
21
22
|
end
|
|
@@ -82,6 +82,7 @@ module GraphQL
|
|
|
82
82
|
replace_late_bound_types_with_built_in(types)
|
|
83
83
|
|
|
84
84
|
schema_extensions = nil
|
|
85
|
+
definitions_by_name = nil
|
|
85
86
|
document.definitions.each do |definition|
|
|
86
87
|
case definition
|
|
87
88
|
when GraphQL::Language::Nodes::SchemaDefinition, GraphQL::Language::Nodes::DirectiveDefinition
|
|
@@ -101,9 +102,16 @@ module GraphQL
|
|
|
101
102
|
if prev_type.nil? || prev_type.is_a?(Schema::LateBoundType)
|
|
102
103
|
if definition.is_a?(GraphQL::Language::Nodes::ObjectTypeDefinition) || definition.is_a?(Language::Nodes::InterfaceTypeDefinition)
|
|
103
104
|
interface_names = definition.interfaces.map(&:name)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
if !interface_names.empty?
|
|
106
|
+
definitions_by_name ||= document.definitions.each_with_object({}) do |d, by_name|
|
|
107
|
+
by_name[d.name] ||= d if d.respond_to?(:name)
|
|
108
|
+
end
|
|
109
|
+
transitive_names = interface_names.map { |n| definitions_by_name[n]&.interfaces&.map(&:name) }
|
|
110
|
+
transitive_names.flatten!
|
|
111
|
+
transitive_names.compact!
|
|
112
|
+
else
|
|
113
|
+
transitive_names = interface_names
|
|
114
|
+
end
|
|
107
115
|
if !(missing_transitive_interfaces = transitive_names - interface_names).empty?
|
|
108
116
|
raise GraphQL::Schema::InvalidDocumentError, "type #{definition.name} is missing one or more transitive interface names: #{missing_transitive_interfaces.join(", ")}. Add them to the type's `implements` list and try again."
|
|
109
117
|
end
|
|
@@ -168,6 +168,9 @@ module GraphQL
|
|
|
168
168
|
# Let validation handle this
|
|
169
169
|
value
|
|
170
170
|
end
|
|
171
|
+
elsif arg_defn.default_value?
|
|
172
|
+
value = arg_defn.default_value
|
|
173
|
+
graphql_value = arg_type.coerce_isolated_result(value) unless value.nil?
|
|
171
174
|
else
|
|
172
175
|
value = graphql_value = nil
|
|
173
176
|
end
|
|
@@ -172,12 +172,12 @@ module GraphQL
|
|
|
172
172
|
types = ctx.types
|
|
173
173
|
|
|
174
174
|
if input.is_a?(Array)
|
|
175
|
-
return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input
|
|
175
|
+
return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input) })
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
if !(input.respond_to?(:to_h) || input.respond_to?(:to_unsafe_h))
|
|
179
179
|
# We're not sure it'll act like a hash, so reject it:
|
|
180
|
-
return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input
|
|
180
|
+
return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input) })
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
|
|
@@ -83,7 +83,7 @@ module GraphQL
|
|
|
83
83
|
is_authed, new_return_value = authorized?(**@prepared_arguments)
|
|
84
84
|
rescue GraphQL::UnauthorizedError => err
|
|
85
85
|
new_return_value = q.schema.unauthorized_object(err)
|
|
86
|
-
is_authed =
|
|
86
|
+
is_authed = false
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
data/lib/graphql/schema.rb
CHANGED
|
@@ -665,8 +665,8 @@ module GraphQL
|
|
|
665
665
|
inherited_um = find_inherited_value(:union_memberships, EMPTY_HASH).fetch(type.graphql_name, EMPTY_ARRAY)
|
|
666
666
|
own_um + inherited_um
|
|
667
667
|
else
|
|
668
|
-
joined_um = own_union_memberships.dup
|
|
669
|
-
find_inherited_value(:
|
|
668
|
+
joined_um = own_union_memberships.transform_values(&:dup)
|
|
669
|
+
find_inherited_value(:union_memberships, EMPTY_HASH).each do |k, v|
|
|
670
670
|
um = joined_um[k] ||= []
|
|
671
671
|
um.concat(v)
|
|
672
672
|
end
|
|
@@ -860,7 +860,7 @@ module GraphQL
|
|
|
860
860
|
# @return [Array<GraphQL::StaticValidation::Error >]
|
|
861
861
|
def validate(string_or_document, rules: nil, context: nil)
|
|
862
862
|
doc = if string_or_document.is_a?(String)
|
|
863
|
-
GraphQL.parse(string_or_document)
|
|
863
|
+
GraphQL.parse(string_or_document, max_tokens: max_query_string_tokens)
|
|
864
864
|
else
|
|
865
865
|
string_or_document
|
|
866
866
|
end
|
|
@@ -1338,9 +1338,9 @@ module GraphQL
|
|
|
1338
1338
|
|
|
1339
1339
|
context.errors << execution_error
|
|
1340
1340
|
execution_error
|
|
1341
|
-
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
|
|
1341
|
+
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::FloatEncodingError, GraphQL::IntegerEncodingError
|
|
1342
1342
|
raise type_error
|
|
1343
|
-
when GraphQL::IntegerDecodingError
|
|
1343
|
+
when GraphQL::FloatDecodingError, GraphQL::IntegerDecodingError
|
|
1344
1344
|
nil
|
|
1345
1345
|
end
|
|
1346
1346
|
end
|
|
@@ -15,6 +15,8 @@ module GraphQL
|
|
|
15
15
|
# separately (which leads to exponential recursion through nested fragments),
|
|
16
16
|
# we flatten all fragment spreads into a single field map and compare within it.
|
|
17
17
|
NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
|
|
18
|
+
EXCLUSIVE_COMPARISON = 1
|
|
19
|
+
NONEXCLUSIVE_COMPARISON = 2
|
|
18
20
|
|
|
19
21
|
class Field
|
|
20
22
|
attr_reader :node, :definition, :owner_type, :parents
|
|
@@ -33,6 +35,10 @@ module GraphQL
|
|
|
33
35
|
def unwrapped_return_type
|
|
34
36
|
@unwrapped_return_type ||= return_type&.unwrap
|
|
35
37
|
end
|
|
38
|
+
|
|
39
|
+
def comparison_key
|
|
40
|
+
@comparison_key ||= [@node, @parents]
|
|
41
|
+
end
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def initialize(*)
|
|
@@ -43,6 +49,9 @@ module GraphQL
|
|
|
43
49
|
# Track which sub-selection node pairs have been compared to prevent
|
|
44
50
|
# infinite recursion with cyclic fragments
|
|
45
51
|
@compared_sub_selections = {}.compare_by_identity
|
|
52
|
+
@compared_field_groups = {}
|
|
53
|
+
@field_group_signatures = {}.compare_by_identity
|
|
54
|
+
@field_selection_signatures = {}.compare_by_identity
|
|
46
55
|
# Cache mutually_exclusive? results for type pairs
|
|
47
56
|
@mutually_exclusive_cache = {}.compare_by_identity
|
|
48
57
|
# Cache collect_fields results for sub-selection comparison
|
|
@@ -215,21 +224,7 @@ module GraphQL
|
|
|
215
224
|
end
|
|
216
225
|
|
|
217
226
|
if all_same
|
|
218
|
-
|
|
219
|
-
# sub-selections. Deduplicate by AST node identity — fields from
|
|
220
|
-
# the same node always have identical sub-selections.
|
|
221
|
-
unique_nodes = fields.uniq { |f| f.node.object_id }
|
|
222
|
-
i = 0
|
|
223
|
-
while i < unique_nodes.size
|
|
224
|
-
j = i + 1
|
|
225
|
-
while j < unique_nodes.size
|
|
226
|
-
if unique_nodes[i].node.selections.size > 0 || unique_nodes[j].node.selections.size > 0
|
|
227
|
-
find_conflict(key, unique_nodes[i], unique_nodes[j])
|
|
228
|
-
end
|
|
229
|
-
j += 1
|
|
230
|
-
end
|
|
231
|
-
i += 1
|
|
232
|
-
end
|
|
227
|
+
find_conflicts_between_selection_groups(key, fields)
|
|
233
228
|
else
|
|
234
229
|
groups = fields.group_by { |f| field_signature(f) }
|
|
235
230
|
unique_groups = groups.values
|
|
@@ -243,22 +238,10 @@ module GraphQL
|
|
|
243
238
|
gj += 1
|
|
244
239
|
end
|
|
245
240
|
|
|
246
|
-
# Within same group,
|
|
247
|
-
# pairs for sub-selection conflicts
|
|
241
|
+
# Within the same group, fields can only conflict on sub-selections.
|
|
248
242
|
group = unique_groups[gi]
|
|
249
243
|
if group.size >= 2
|
|
250
|
-
|
|
251
|
-
ui = 0
|
|
252
|
-
while ui < unique_in_group.size
|
|
253
|
-
uj = ui + 1
|
|
254
|
-
while uj < unique_in_group.size
|
|
255
|
-
if unique_in_group[ui].node.selections.size > 0 || unique_in_group[uj].node.selections.size > 0
|
|
256
|
-
find_conflict(key, unique_in_group[ui], unique_in_group[uj])
|
|
257
|
-
end
|
|
258
|
-
uj += 1
|
|
259
|
-
end
|
|
260
|
-
ui += 1
|
|
261
|
-
end
|
|
244
|
+
find_conflicts_between_selection_groups(key, group)
|
|
262
245
|
end
|
|
263
246
|
|
|
264
247
|
gi += 1
|
|
@@ -279,6 +262,29 @@ module GraphQL
|
|
|
279
262
|
end
|
|
280
263
|
end
|
|
281
264
|
|
|
265
|
+
def find_conflicts_between_selection_groups(response_key, fields)
|
|
266
|
+
fields_by_selection = {}
|
|
267
|
+
fields.each do |field|
|
|
268
|
+
fields_by_selection[field_selection_signature(field)] ||= field
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
representatives = fields_by_selection.values
|
|
272
|
+
i = 0
|
|
273
|
+
while i < representatives.size
|
|
274
|
+
j = i + 1
|
|
275
|
+
while j < representatives.size
|
|
276
|
+
find_conflict(response_key, representatives[i], representatives[j])
|
|
277
|
+
j += 1
|
|
278
|
+
end
|
|
279
|
+
i += 1
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def field_selection_signature(field)
|
|
284
|
+
node = field.node
|
|
285
|
+
@field_selection_signatures[node] ||= node.selections.map(&:to_query_string)
|
|
286
|
+
end
|
|
287
|
+
|
|
282
288
|
def fields_same_signature?(f1, f2)
|
|
283
289
|
n1 = f1.node
|
|
284
290
|
n2 = f2.node
|
|
@@ -458,6 +464,7 @@ module GraphQL
|
|
|
458
464
|
response_keys.each do |key, fields|
|
|
459
465
|
fields2 = response_keys2[key]
|
|
460
466
|
next unless fields2
|
|
467
|
+
next if field_groups_already_compared?(fields, fields2, mutually_exclusive)
|
|
461
468
|
|
|
462
469
|
fields_arr = fields.is_a?(Field) ? [fields] : fields
|
|
463
470
|
fields2_arr = fields2.is_a?(Field) ? [fields2] : fields2
|
|
@@ -475,6 +482,36 @@ module GraphQL
|
|
|
475
482
|
end
|
|
476
483
|
end
|
|
477
484
|
|
|
485
|
+
def field_groups_already_compared?(fields, fields2, mutually_exclusive)
|
|
486
|
+
signature1 = field_group_signature(fields)
|
|
487
|
+
signature2 = field_group_signature(fields2)
|
|
488
|
+
previous_comparisons = @compared_field_groups[signature1]
|
|
489
|
+
comparison_state = previous_comparisons && previous_comparisons[signature2]
|
|
490
|
+
|
|
491
|
+
if mutually_exclusive
|
|
492
|
+
return true if comparison_state
|
|
493
|
+
new_state = EXCLUSIVE_COMPARISON
|
|
494
|
+
else
|
|
495
|
+
return true if comparison_state == NONEXCLUSIVE_COMPARISON
|
|
496
|
+
new_state = NONEXCLUSIVE_COMPARISON
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
previous_comparisons ||= (@compared_field_groups[signature1] = {})
|
|
500
|
+
previous_comparisons[signature2] = new_state
|
|
501
|
+
|
|
502
|
+
reverse_comparisons = @compared_field_groups[signature2] ||= {}
|
|
503
|
+
reverse_comparisons[signature1] = new_state
|
|
504
|
+
false
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def field_group_signature(fields)
|
|
508
|
+
if fields.is_a?(Field)
|
|
509
|
+
fields.comparison_key
|
|
510
|
+
else
|
|
511
|
+
@field_group_signatures[fields] ||= fields.map(&:comparison_key)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
478
515
|
def same_arguments?(field1, field2)
|
|
479
516
|
arguments1 = field1.arguments
|
|
480
517
|
arguments2 = field2.arguments
|