graphql 1.12.19 → 1.12.20
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphql/relay.rb +19 -11
- data/lib/generators/graphql/templates/schema.erb +13 -1
- data/lib/graphql/dataloader/source.rb +30 -2
- data/lib/graphql/deprecation.rb +1 -5
- data/lib/graphql/pagination/connections.rb +35 -16
- data/lib/graphql/static_validation/error.rb +3 -1
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +40 -21
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +25 -4
- data/lib/graphql/static_validation/validation_context.rb +2 -1
- data/lib/graphql/subscriptions/event.rb +46 -2
- 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: b8312601f8e51973aaa9418c4c4691b6eab97140351c91d47f6c2c4f4deb88fb
|
4
|
+
data.tar.gz: 4cb698ef0a6739ceca4026b4dfaa594faf5658ac10a6005b051b70ec34af7f41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9122ecfa0c4c28d219af2c828adb1b5e7e0ae29b10df55f543cbfa8973d8d9706ba5258c3de4f620b6cba5f16d6e6e031ebb6605193153ce5be821eaeb1adaa
|
7
|
+
data.tar.gz: 4db8f3435262d72f1d0d1995407f83c76a55dc0b5f28f7b5d8c6c4a378b2da00e4119e371585bdfac847ae5661c6e91e3bfdfcde82cd232198456905dee1f1a0
|
@@ -32,20 +32,28 @@ module Graphql
|
|
32
32
|
|
33
33
|
# Return a string UUID for `object`
|
34
34
|
def self.id_from_object(object, type_definition, query_ctx)
|
35
|
-
#
|
36
|
-
|
37
|
-
#
|
38
|
-
|
35
|
+
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
|
36
|
+
object_id = object.to_global_id.to_s
|
37
|
+
# Remove this redundant prefix to make IDs shorter:
|
38
|
+
object_id = object_id.sub("gid://\#{GlobalID.app}/", "")
|
39
|
+
encoded_id = Base64.urlsafe_encode64(object_id)
|
40
|
+
# Remove the "=" padding
|
41
|
+
encoded_id = encoded_id.sub(/=+/, "")
|
42
|
+
# Add a type hint
|
43
|
+
type_hint = type_definition.graphql_name.first
|
44
|
+
"\#{type_hint}_\#{encoded_id}"
|
39
45
|
end
|
40
46
|
|
41
47
|
# Given a string UUID, find the object
|
42
|
-
def self.object_from_id(
|
43
|
-
# For example,
|
44
|
-
#
|
45
|
-
|
46
|
-
#
|
47
|
-
|
48
|
-
#
|
48
|
+
def self.object_from_id(encoded_id_with_hint, query_ctx)
|
49
|
+
# For example, use Rails' GlobalID library (https://github.com/rails/globalid):
|
50
|
+
# Split off the type hint
|
51
|
+
_type_hint, encoded_id = encoded_id_with_hint.split("_", 2)
|
52
|
+
# Decode the ID
|
53
|
+
id = Base64.urlsafe_decode64(encoded_id)
|
54
|
+
# Rebuild it for Rails then find the object:
|
55
|
+
full_global_id = "gid://\#{GlobalID.app}/\#{id}"
|
56
|
+
GlobalID::Locator.locate(full_global_id)
|
49
57
|
end
|
50
58
|
RUBY
|
51
59
|
inject_into_file schema_file_path, schema_code, before: /^end\n/m, force: false
|
@@ -4,11 +4,23 @@ class <%= schema_name %> < GraphQL::Schema
|
|
4
4
|
<% if options[:batch] %>
|
5
5
|
# GraphQL::Batch setup:
|
6
6
|
use GraphQL::Batch
|
7
|
+
<% else %>
|
8
|
+
# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
|
9
|
+
use GraphQL::Dataloader
|
7
10
|
<% end %>
|
11
|
+
# GraphQL-Ruby calls this when something goes wrong while running a query:
|
12
|
+
def self.type_error(err)
|
13
|
+
# if err.is_a?(GraphQL::InvalidNullError)
|
14
|
+
# # report to your bug tracker here
|
15
|
+
# return nil
|
16
|
+
# end
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
8
20
|
# Union and Interface Resolution
|
9
21
|
def self.resolve_type(abstract_type, obj, ctx)
|
10
22
|
# TODO: Implement this function
|
11
|
-
# to return the correct object type for `obj`
|
23
|
+
# to return the correct GraphQL object type for `obj`
|
12
24
|
raise(GraphQL::RequiredImplementationMissingError)
|
13
25
|
end
|
14
26
|
end
|
@@ -6,7 +6,11 @@ module GraphQL
|
|
6
6
|
# Called by {Dataloader} to prepare the {Source}'s internal state
|
7
7
|
# @api private
|
8
8
|
def setup(dataloader)
|
9
|
+
# These keys have been requested but haven't been fetched yet
|
9
10
|
@pending_keys = []
|
11
|
+
# These keys have been passed to `fetch` but haven't been finished yet
|
12
|
+
@fetching_keys = []
|
13
|
+
# { key => result }
|
10
14
|
@results = {}
|
11
15
|
@dataloader = dataloader
|
12
16
|
end
|
@@ -64,29 +68,46 @@ module GraphQL
|
|
64
68
|
# Then run the batch and update the cache.
|
65
69
|
# @return [void]
|
66
70
|
def sync
|
71
|
+
pending_keys = @pending_keys.dup
|
67
72
|
@dataloader.yield
|
73
|
+
iterations = 0
|
74
|
+
while pending_keys.any? { |k| !@results.key?(k) }
|
75
|
+
iterations += 1
|
76
|
+
if iterations > 1000
|
77
|
+
raise "#{self.class}#sync tried 1000 times to load pending keys (#{pending_keys}), but they still weren't loaded. There is likely a circular dependency."
|
78
|
+
end
|
79
|
+
@dataloader.yield
|
80
|
+
end
|
81
|
+
nil
|
68
82
|
end
|
69
83
|
|
70
84
|
# @return [Boolean] True if this source has any pending requests for data.
|
71
85
|
def pending?
|
72
|
-
|
86
|
+
!@pending_keys.empty?
|
73
87
|
end
|
74
88
|
|
75
89
|
# Called by {GraphQL::Dataloader} to resolve and pending requests to this source.
|
76
90
|
# @api private
|
77
91
|
# @return [void]
|
78
92
|
def run_pending_keys
|
93
|
+
if !@fetching_keys.empty?
|
94
|
+
@pending_keys -= @fetching_keys
|
95
|
+
end
|
79
96
|
return if @pending_keys.empty?
|
80
97
|
fetch_keys = @pending_keys.uniq
|
98
|
+
@fetching_keys.concat(fetch_keys)
|
81
99
|
@pending_keys = []
|
82
100
|
results = fetch(fetch_keys)
|
83
101
|
fetch_keys.each_with_index do |key, idx|
|
84
102
|
@results[key] = results[idx]
|
85
103
|
end
|
104
|
+
nil
|
86
105
|
rescue StandardError => error
|
87
106
|
fetch_keys.each { |key| @results[key] = error }
|
88
107
|
ensure
|
89
|
-
|
108
|
+
if fetch_keys
|
109
|
+
@fetching_keys -= fetch_keys
|
110
|
+
end
|
90
111
|
end
|
91
112
|
|
92
113
|
# These arguments are given to `dataloader.with(source_class, ...)`. The object
|
@@ -116,6 +137,13 @@ module GraphQL
|
|
116
137
|
# @return [Object] The result from {#fetch} for `key`.
|
117
138
|
# @api private
|
118
139
|
def result_for(key)
|
140
|
+
if !@results.key?(key)
|
141
|
+
raise <<-ERR
|
142
|
+
Invariant: fetching result for a key on #{self.class} that hasn't been loaded yet (#{key.inspect}, loaded: #{@results.keys})
|
143
|
+
|
144
|
+
This key should have been loaded already. This is a bug in GraphQL::Dataloader, please report it on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/new.
|
145
|
+
ERR
|
146
|
+
end
|
119
147
|
result = @results[key]
|
120
148
|
|
121
149
|
raise result if result.class <= StandardError
|
data/lib/graphql/deprecation.rb
CHANGED
@@ -70,23 +70,42 @@ module GraphQL
|
|
70
70
|
wrappers = context ? context.namespace(:connections)[:all_wrappers] : all_wrappers
|
71
71
|
impl = wrapper_for(items, wrappers: wrappers)
|
72
72
|
|
73
|
-
if impl
|
74
|
-
|
73
|
+
if impl
|
74
|
+
impl.new(
|
75
|
+
items,
|
76
|
+
context: context,
|
77
|
+
parent: parent,
|
78
|
+
field: field,
|
79
|
+
max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
|
80
|
+
first: arguments[:first],
|
81
|
+
after: arguments[:after],
|
82
|
+
last: arguments[:last],
|
83
|
+
before: arguments[:before],
|
84
|
+
arguments: arguments,
|
85
|
+
edge_class: edge_class_for_field(field),
|
86
|
+
)
|
87
|
+
else
|
88
|
+
begin
|
89
|
+
connection_class = GraphQL::Relay::BaseConnection.connection_for_nodes(items)
|
90
|
+
if parent.is_a?(GraphQL::Schema::Object)
|
91
|
+
parent = parent.object
|
92
|
+
end
|
93
|
+
connection_class.new(
|
94
|
+
items,
|
95
|
+
arguments,
|
96
|
+
field: field,
|
97
|
+
max_page_size: field.max_page_size,
|
98
|
+
parent: parent,
|
99
|
+
context: context,
|
100
|
+
)
|
101
|
+
rescue RuntimeError => err
|
102
|
+
if err.message.include?("No connection implementation to wrap")
|
103
|
+
raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
|
104
|
+
else
|
105
|
+
raise err
|
106
|
+
end
|
107
|
+
end
|
75
108
|
end
|
76
|
-
|
77
|
-
impl.new(
|
78
|
-
items,
|
79
|
-
context: context,
|
80
|
-
parent: parent,
|
81
|
-
field: field,
|
82
|
-
max_page_size: field.has_max_page_size? ? field.max_page_size : context.schema.default_max_page_size,
|
83
|
-
first: arguments[:first],
|
84
|
-
after: arguments[:after],
|
85
|
-
last: arguments[:last],
|
86
|
-
before: arguments[:before],
|
87
|
-
arguments: arguments,
|
88
|
-
edge_class: edge_class_for_field(field),
|
89
|
-
)
|
90
109
|
end
|
91
110
|
|
92
111
|
# use an override if there is one
|
@@ -10,6 +10,7 @@ module GraphQL
|
|
10
10
|
#
|
11
11
|
# Original Algorithm: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js
|
12
12
|
NO_ARGS = {}.freeze
|
13
|
+
|
13
14
|
Field = Struct.new(:node, :definition, :owner_type, :parents)
|
14
15
|
FragmentSpread = Struct.new(:name, :parents)
|
15
16
|
|
@@ -17,20 +18,43 @@ module GraphQL
|
|
17
18
|
super
|
18
19
|
@visited_fragments = {}
|
19
20
|
@compared_fragments = {}
|
21
|
+
@conflict_count = 0
|
20
22
|
end
|
21
23
|
|
22
24
|
def on_operation_definition(node, _parent)
|
23
|
-
conflicts_within_selection_set(node, type_definition)
|
25
|
+
setting_errors { conflicts_within_selection_set(node, type_definition) }
|
24
26
|
super
|
25
27
|
end
|
26
28
|
|
27
29
|
def on_field(node, _parent)
|
28
|
-
conflicts_within_selection_set(node, type_definition)
|
30
|
+
setting_errors { conflicts_within_selection_set(node, type_definition) }
|
29
31
|
super
|
30
32
|
end
|
31
33
|
|
32
34
|
private
|
33
35
|
|
36
|
+
def field_conflicts
|
37
|
+
@field_conflicts ||= Hash.new do |errors, field|
|
38
|
+
errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :field, field_name: field)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def arg_conflicts
|
43
|
+
@arg_conflicts ||= Hash.new do |errors, field|
|
44
|
+
errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :argument, field_name: field)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def setting_errors
|
49
|
+
@field_conflicts = nil
|
50
|
+
@arg_conflicts = nil
|
51
|
+
|
52
|
+
yield
|
53
|
+
|
54
|
+
field_conflicts.each_value { |error| add_error(error) }
|
55
|
+
arg_conflicts.each_value { |error| add_error(error) }
|
56
|
+
end
|
57
|
+
|
34
58
|
def conflicts_within_selection_set(node, parent_type)
|
35
59
|
return if parent_type.nil?
|
36
60
|
|
@@ -183,6 +207,8 @@ module GraphQL
|
|
183
207
|
end
|
184
208
|
|
185
209
|
def find_conflict(response_key, field1, field2, mutually_exclusive: false)
|
210
|
+
return if @conflict_count >= context.max_errors
|
211
|
+
|
186
212
|
node1 = field1.node
|
187
213
|
node2 = field2.node
|
188
214
|
|
@@ -191,28 +217,21 @@ module GraphQL
|
|
191
217
|
|
192
218
|
if !are_mutually_exclusive
|
193
219
|
if node1.name != node2.name
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
field_name: response_key,
|
201
|
-
conflicts: errored_nodes
|
202
|
-
))
|
220
|
+
conflict = field_conflicts[response_key]
|
221
|
+
|
222
|
+
conflict.add_conflict(node1, node1.name)
|
223
|
+
conflict.add_conflict(node2, node2.name)
|
224
|
+
|
225
|
+
@conflict_count += 1
|
203
226
|
end
|
204
227
|
|
205
228
|
if !same_arguments?(node1, node2)
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
path: [],
|
213
|
-
field_name: response_key,
|
214
|
-
conflicts: conflicts
|
215
|
-
))
|
229
|
+
conflict = arg_conflicts[response_key]
|
230
|
+
|
231
|
+
conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
|
232
|
+
conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))
|
233
|
+
|
234
|
+
@conflict_count += 1
|
216
235
|
end
|
217
236
|
end
|
218
237
|
|
@@ -3,12 +3,33 @@ module GraphQL
|
|
3
3
|
module StaticValidation
|
4
4
|
class FieldsWillMergeError < StaticValidation::Error
|
5
5
|
attr_reader :field_name
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :kind
|
7
|
+
|
8
|
+
def initialize(kind:, field_name:)
|
9
|
+
super(nil)
|
7
10
|
|
8
|
-
def initialize(message, path: nil, nodes: [], field_name:, conflicts:)
|
9
|
-
super(message, path: path, nodes: nodes)
|
10
11
|
@field_name = field_name
|
11
|
-
@
|
12
|
+
@kind = kind
|
13
|
+
@conflicts = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
|
18
|
+
end
|
19
|
+
|
20
|
+
def path
|
21
|
+
[]
|
22
|
+
end
|
23
|
+
|
24
|
+
def conflicts
|
25
|
+
@conflicts.join(' or ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_conflict(node, conflict_str)
|
29
|
+
return if nodes.include?(node)
|
30
|
+
|
31
|
+
@nodes << node
|
32
|
+
@conflicts << conflict_str
|
12
33
|
end
|
13
34
|
|
14
35
|
# A hash representation of this Message
|
@@ -53,6 +53,36 @@ module GraphQL
|
|
53
53
|
|
54
54
|
class << self
|
55
55
|
private
|
56
|
+
|
57
|
+
# This method does not support cyclic references in the Hash,
|
58
|
+
# nor does it support Hashes whose keys are not sortable
|
59
|
+
# with respect to their peers ( cases where a <=> b might throw an error )
|
60
|
+
def deep_sort_hash_keys(hash_to_sort)
|
61
|
+
raise ArgumentError.new("Argument must be a Hash") unless hash_to_sort.is_a?(Hash)
|
62
|
+
hash_to_sort.keys.sort.map do |k|
|
63
|
+
if hash_to_sort[k].is_a?(Hash)
|
64
|
+
[k, deep_sort_hash_keys(hash_to_sort[k])]
|
65
|
+
elsif hash_to_sort[k].is_a?(Array)
|
66
|
+
[k, deep_sort_array_hashes(hash_to_sort[k])]
|
67
|
+
else
|
68
|
+
[k, hash_to_sort[k]]
|
69
|
+
end
|
70
|
+
end.to_h
|
71
|
+
end
|
72
|
+
|
73
|
+
def deep_sort_array_hashes(array_to_inspect)
|
74
|
+
raise ArgumentError.new("Argument must be an Array") unless array_to_inspect.is_a?(Array)
|
75
|
+
array_to_inspect.map do |v|
|
76
|
+
if v.is_a?(Hash)
|
77
|
+
deep_sort_hash_keys(v)
|
78
|
+
elsif v.is_a?(Array)
|
79
|
+
deep_sort_array_hashes(v)
|
80
|
+
else
|
81
|
+
v
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
56
86
|
def stringify_args(arg_owner, args)
|
57
87
|
arg_owner = arg_owner.respond_to?(:unwrap) ? arg_owner.unwrap : arg_owner # remove list and non-null wrappers
|
58
88
|
case args
|
@@ -65,12 +95,26 @@ module GraphQL
|
|
65
95
|
|
66
96
|
if arg_defn
|
67
97
|
normalized_arg_name = camelized_arg_name
|
68
|
-
next_args[normalized_arg_name] = stringify_args(arg_defn.type, v)
|
69
98
|
else
|
70
99
|
normalized_arg_name = arg_name
|
71
100
|
arg_defn = get_arg_definition(arg_owner, normalized_arg_name)
|
72
101
|
end
|
73
|
-
|
102
|
+
arg_base_type = arg_defn.type.unwrap
|
103
|
+
# In the case where the value being emitted is seen as a "JSON"
|
104
|
+
# type, treat the value as one atomic unit of serialization
|
105
|
+
is_json_definition = arg_base_type && arg_base_type <= GraphQL::Types::JSON
|
106
|
+
if is_json_definition
|
107
|
+
sorted_value = if v.is_a?(Hash)
|
108
|
+
deep_sort_hash_keys(v)
|
109
|
+
elsif v.is_a?(Array)
|
110
|
+
deep_sort_array_hashes(v)
|
111
|
+
else
|
112
|
+
v
|
113
|
+
end
|
114
|
+
next_args[normalized_arg_name] = sorted_value.respond_to?(:to_json) ? sorted_value.to_json : sorted_value
|
115
|
+
else
|
116
|
+
next_args[normalized_arg_name] = stringify_args(arg_base_type, v)
|
117
|
+
end
|
74
118
|
end
|
75
119
|
# Make sure they're deeply sorted
|
76
120
|
next_args.sort.to_h
|
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.12.
|
4
|
+
version: 1.12.20
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-11-
|
11
|
+
date: 2021-11-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: benchmark-ips
|