graphql-stitching 1.7.2 → 1.8.0
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/README.md +1 -1
- data/Rakefile +11 -1
- data/benchmark/run.rb +279 -0
- data/docs/performance.md +28 -0
- data/graphql-stitching.gemspec +3 -3
- data/lib/graphql/stitching/client.rb +2 -2
- data/lib/graphql/stitching/composer/type_resolver_config.rb +1 -1
- data/lib/graphql/stitching/composer.rb +1 -3
- data/lib/graphql/stitching/executor/path_access.rb +81 -0
- data/lib/graphql/stitching/executor/root_source.rb +59 -40
- data/lib/graphql/stitching/executor/shaper.rb +27 -18
- data/lib/graphql/stitching/executor/type_resolver_source.rb +78 -88
- data/lib/graphql/stitching/executor.rb +2 -1
- data/lib/graphql/stitching/http_executable.rb +1 -2
- data/lib/graphql/stitching/plan.rb +46 -13
- data/lib/graphql/stitching/planner.rb +62 -46
- data/lib/graphql/stitching/request/skip_include.rb +2 -2
- data/lib/graphql/stitching/request.rb +1 -1
- data/lib/graphql/stitching/supergraph.rb +16 -6
- data/lib/graphql/stitching/type_resolver.rb +12 -1
- data/lib/graphql/stitching/util.rb +21 -4
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +11 -9
- /data/docs/{introduction.md → README.md} +0 -0
|
@@ -11,6 +11,7 @@ module GraphQL::Stitching
|
|
|
11
11
|
@request = request
|
|
12
12
|
@supergraph = request.supergraph
|
|
13
13
|
@root_type = nil
|
|
14
|
+
@possible_type_names_by_type = nil
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def perform!(raw)
|
|
@@ -24,16 +25,23 @@ module GraphQL::Stitching
|
|
|
24
25
|
return nil if raw_object.nil?
|
|
25
26
|
|
|
26
27
|
typename ||= raw_object[TypeResolver::TYPENAME_EXPORT_NODE.alias]
|
|
28
|
+
typename ||= parent_type.graphql_name unless parent_type.kind.abstract?
|
|
29
|
+
|
|
27
30
|
raw_object.reject! { |key, _v| TypeResolver.export_key?(key) }
|
|
28
31
|
|
|
29
32
|
selections.each do |node|
|
|
30
33
|
case node
|
|
31
34
|
when GraphQL::Language::Nodes::Field
|
|
32
35
|
field_name = node.alias || node.name
|
|
36
|
+
raw_value = raw_object.delete(field_name)
|
|
33
37
|
|
|
34
38
|
if @request.query.get_field(parent_type, node.name).introspection?
|
|
35
|
-
if
|
|
36
|
-
|
|
39
|
+
next if TypeResolver.export_key?(field_name)
|
|
40
|
+
|
|
41
|
+
raw_object[field_name] = if node.name == TYPENAME && parent_type == @root_type
|
|
42
|
+
@root_type.graphql_name
|
|
43
|
+
else
|
|
44
|
+
raw_value
|
|
37
45
|
end
|
|
38
46
|
next
|
|
39
47
|
end
|
|
@@ -42,14 +50,14 @@ module GraphQL::Stitching
|
|
|
42
50
|
named_type = node_type.unwrap
|
|
43
51
|
|
|
44
52
|
raw_object[field_name] = if node_type.list?
|
|
45
|
-
resolve_list_scope(
|
|
53
|
+
resolve_list_scope(raw_value, Util.unwrap_non_null(node_type), node.selections)
|
|
46
54
|
elsif Util.is_leaf_type?(named_type)
|
|
47
|
-
|
|
55
|
+
raw_value
|
|
48
56
|
else
|
|
49
|
-
resolve_object_scope(
|
|
57
|
+
resolve_object_scope(raw_value, named_type, node.selections)
|
|
50
58
|
end
|
|
51
59
|
|
|
52
|
-
return nil if raw_object[field_name].nil?
|
|
60
|
+
return nil if node_type.non_null? && raw_object[field_name].nil?
|
|
53
61
|
|
|
54
62
|
when GraphQL::Language::Nodes::InlineFragment
|
|
55
63
|
fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
|
|
@@ -79,36 +87,37 @@ module GraphQL::Stitching
|
|
|
79
87
|
|
|
80
88
|
next_node_type = Util.unwrap_non_null(current_node_type).of_type
|
|
81
89
|
named_type = next_node_type.unwrap
|
|
82
|
-
|
|
90
|
+
|
|
91
|
+
if Util.is_leaf_type?(named_type)
|
|
92
|
+
return nil if next_node_type.non_null? && raw_list.include?(nil)
|
|
93
|
+
|
|
94
|
+
return raw_list
|
|
95
|
+
end
|
|
83
96
|
|
|
84
97
|
resolved_list = raw_list.map! do |raw_list_element|
|
|
85
98
|
result = if next_node_type.list?
|
|
86
99
|
resolve_list_scope(raw_list_element, next_node_type, selections)
|
|
87
|
-
elsif Util.is_leaf_type?(named_type)
|
|
88
|
-
raw_list_element
|
|
89
100
|
else
|
|
90
101
|
resolve_object_scope(raw_list_element, named_type, selections)
|
|
91
102
|
end
|
|
92
103
|
|
|
93
|
-
if result.nil?
|
|
94
|
-
contains_null = true
|
|
95
|
-
return nil if current_node_type.non_null?
|
|
96
|
-
end
|
|
104
|
+
return nil if result.nil? && next_node_type.non_null?
|
|
97
105
|
|
|
98
106
|
result
|
|
99
107
|
end
|
|
100
108
|
|
|
101
|
-
return nil if contains_null && next_node_type.non_null?
|
|
102
|
-
|
|
103
109
|
resolved_list
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
def typename_in_type?(typename, type)
|
|
107
113
|
return true if type.graphql_name == typename
|
|
114
|
+
return false unless typename && type.kind.abstract?
|
|
108
115
|
|
|
109
|
-
type.
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
possible_type_names(type).include?(typename)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def possible_type_names(type)
|
|
120
|
+
(@possible_type_names_by_type ||= {})[type.graphql_name] ||= @request.query.possible_types(type).map(&:graphql_name)
|
|
112
121
|
end
|
|
113
122
|
end
|
|
114
123
|
end
|
|
@@ -3,40 +3,39 @@
|
|
|
3
3
|
module GraphQL::Stitching
|
|
4
4
|
class Executor
|
|
5
5
|
class TypeResolverSource < GraphQL::Dataloader::Source
|
|
6
|
+
include PathAccess
|
|
7
|
+
|
|
6
8
|
def initialize(executor, location)
|
|
7
9
|
@executor = executor
|
|
8
10
|
@location = location
|
|
9
|
-
@variables = {}
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def fetch(ops)
|
|
13
14
|
origin_sets_by_operation = ops.each_with_object({}.compare_by_identity) do |op, memo|
|
|
14
|
-
origin_set =
|
|
15
|
-
set.flat_map { |obj| obj && obj[path_segment] }.tap(&:compact!)
|
|
16
|
-
end
|
|
15
|
+
origin_set = path_objects(@executor.data, op.path)
|
|
17
16
|
|
|
18
17
|
if op.if_type
|
|
19
18
|
# operations planned around unused fragment conditions should not trigger requests
|
|
20
|
-
origin_set.select! {
|
|
19
|
+
origin_set.select! { |origin_obj| origin_obj[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
memo[op] = origin_set
|
|
22
|
+
memo[op] = origin_set unless origin_set.empty?
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
query_document, variable_names = build_document(
|
|
25
|
+
unless origin_sets_by_operation.empty?
|
|
26
|
+
query_document, variable_names, generated_variables = build_document(
|
|
28
27
|
origin_sets_by_operation,
|
|
29
28
|
@executor.request.operation_name,
|
|
30
29
|
@executor.request.operation_directives,
|
|
31
30
|
)
|
|
32
|
-
variables =
|
|
31
|
+
variables = generated_variables.merge(@executor.request.variables.slice(*variable_names))
|
|
33
32
|
raw_result = @executor.request.supergraph.execute_at_location(@location, query_document, variables, @executor.request)
|
|
34
33
|
@executor.query_count += 1
|
|
35
34
|
|
|
36
35
|
merge_results!(origin_sets_by_operation, raw_result.dig("data"))
|
|
37
36
|
|
|
38
37
|
errors = raw_result.dig("errors")
|
|
39
|
-
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors
|
|
38
|
+
@executor.errors.concat(extract_errors!(origin_sets_by_operation, errors)) if errors && !errors.empty?
|
|
40
39
|
end
|
|
41
40
|
|
|
42
41
|
ops.map { origin_sets_by_operation[_1] ? _1.step : nil }
|
|
@@ -51,63 +50,81 @@ module GraphQL::Stitching
|
|
|
51
50
|
# }"
|
|
52
51
|
def build_document(origin_sets_by_operation, operation_name = nil, operation_directives = nil)
|
|
53
52
|
variable_defs = {}
|
|
54
|
-
|
|
53
|
+
generated_variables = {}
|
|
54
|
+
fields_buffer = String.new
|
|
55
|
+
|
|
56
|
+
origin_sets_by_operation.each_with_index do |(op, origin_set), batch_index|
|
|
55
57
|
variable_defs.merge!(op.variables)
|
|
56
58
|
resolver = @executor.request.supergraph.resolvers_by_version[op.resolver]
|
|
59
|
+
fields_buffer << " " unless batch_index.zero?
|
|
57
60
|
|
|
58
61
|
if resolver.list?
|
|
59
|
-
|
|
62
|
+
fields_buffer << "_" << batch_index.to_s << "_result: " << resolver.field << "("
|
|
63
|
+
|
|
64
|
+
resolver.arguments.each_with_index do |arg, i|
|
|
65
|
+
fields_buffer << "," unless i.zero?
|
|
60
66
|
if arg.key?
|
|
61
67
|
variable_name = "_#{batch_index}_key_#{i}".freeze
|
|
62
|
-
|
|
68
|
+
generated_variables[variable_name] = origin_set.map { arg.build(_1) }
|
|
63
69
|
variable_defs[variable_name] = arg.to_type_signature
|
|
64
|
-
|
|
70
|
+
fields_buffer << arg.name << ":$" << variable_name
|
|
65
71
|
else
|
|
66
|
-
|
|
72
|
+
fields_buffer << arg.name << ":" << arg.value.print
|
|
67
73
|
end
|
|
68
74
|
end
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
fields_buffer << ") " << op.selections
|
|
71
77
|
else
|
|
72
|
-
origin_set.
|
|
73
|
-
|
|
78
|
+
origin_set.each_with_index do |origin_obj, index|
|
|
79
|
+
fields_buffer << " " unless index.zero?
|
|
80
|
+
fields_buffer << "_" << batch_index.to_s << "_" << index.to_s << "_result: " << resolver.field << "("
|
|
81
|
+
|
|
82
|
+
resolver.arguments.each_with_index do |arg, i|
|
|
83
|
+
fields_buffer << "," unless i.zero?
|
|
74
84
|
if arg.key?
|
|
75
85
|
variable_name = "_#{batch_index}_#{index}_key_#{i}".freeze
|
|
76
|
-
|
|
86
|
+
generated_variables[variable_name] = arg.build(origin_obj)
|
|
77
87
|
variable_defs[variable_name] = arg.to_type_signature
|
|
78
|
-
|
|
88
|
+
fields_buffer << arg.name << ":$" << variable_name
|
|
79
89
|
else
|
|
80
|
-
|
|
90
|
+
fields_buffer << arg.name << ":" << arg.value.print
|
|
81
91
|
end
|
|
82
92
|
end
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
fields_buffer << ") " << op.selections
|
|
85
95
|
end
|
|
86
96
|
end
|
|
87
97
|
end
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
doc_buffer = String.new(QUERY_OP) # << resolver fulfillment always uses query
|
|
90
100
|
|
|
91
101
|
if operation_name
|
|
92
|
-
|
|
102
|
+
doc_buffer << " " << operation_name
|
|
93
103
|
origin_sets_by_operation.each_key do |op|
|
|
94
|
-
|
|
104
|
+
doc_buffer << "_" << op.step.to_s
|
|
95
105
|
end
|
|
96
106
|
end
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
unless variable_defs.empty?
|
|
109
|
+
doc_buffer << "("
|
|
110
|
+
variable_defs.each_with_index do |(k, v), i|
|
|
111
|
+
doc_buffer << "," unless i.zero?
|
|
112
|
+
doc_buffer << "$" << k << ":" << v
|
|
113
|
+
end
|
|
114
|
+
doc_buffer << ")"
|
|
100
115
|
end
|
|
101
116
|
|
|
102
117
|
if operation_directives
|
|
103
|
-
|
|
118
|
+
doc_buffer << " " << operation_directives << " "
|
|
104
119
|
end
|
|
105
120
|
|
|
106
|
-
|
|
121
|
+
doc_buffer << "{ " << fields_buffer << " }"
|
|
107
122
|
|
|
108
|
-
|
|
109
|
-
names.reject! {
|
|
123
|
+
variable_names = variable_defs.keys.tap do |names|
|
|
124
|
+
names.reject! { generated_variables.key?(_1) }
|
|
110
125
|
end
|
|
126
|
+
|
|
127
|
+
return doc_buffer, variable_names, generated_variables
|
|
111
128
|
end
|
|
112
129
|
|
|
113
130
|
def merge_results!(origin_sets_by_operation, raw_result)
|
|
@@ -120,94 +137,67 @@ module GraphQL::Stitching
|
|
|
120
137
|
origin_set.map.with_index { |_, index| raw_result["_#{batch_index}_#{index}_result"] }
|
|
121
138
|
end
|
|
122
139
|
|
|
123
|
-
next
|
|
140
|
+
next if results.nil? || results.empty?
|
|
124
141
|
|
|
125
142
|
origin_set.each_with_index do |origin_obj, index|
|
|
126
|
-
|
|
143
|
+
result = results[index]
|
|
144
|
+
origin_obj.merge!(result) if result
|
|
127
145
|
end
|
|
128
146
|
end
|
|
129
147
|
end
|
|
130
148
|
|
|
131
149
|
# https://spec.graphql.org/June2018/#sec-Errors
|
|
132
|
-
def extract_errors!(origin_sets_by_operation, errors)
|
|
150
|
+
def extract_errors!(origin_sets_by_operation, errors, origin_paths_by_operation = nil)
|
|
133
151
|
ops = origin_sets_by_operation.keys
|
|
134
152
|
origin_sets = origin_sets_by_operation.values
|
|
135
|
-
|
|
153
|
+
origin_paths_by_operation ||= origin_sets_by_operation.each_with_object({}.compare_by_identity) do |(op, origin_set), memo|
|
|
154
|
+
memo[op] = paths_for_origin_set(op, origin_set)
|
|
155
|
+
end
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
err.delete("locations")
|
|
157
|
+
errors.each_with_object([]) do |err, memo|
|
|
139
158
|
path = err["path"]
|
|
140
159
|
|
|
141
160
|
if path && path.length > 0
|
|
142
161
|
result_alias = /^_(\d+)(?:_(\d+))?_result$/.match(path.first.to_s)
|
|
143
162
|
|
|
144
163
|
if result_alias
|
|
145
|
-
path =
|
|
164
|
+
path = path[1..-1]
|
|
165
|
+
batch_index = result_alias[1].to_i
|
|
146
166
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
elsif path[0].is_a?(Integer) || /\d
|
|
150
|
-
|
|
167
|
+
origin_index = if result_alias[2]
|
|
168
|
+
result_alias[2].to_i
|
|
169
|
+
elsif path[0].is_a?(Integer) || /\A\d+\z/.match?(path[0].to_s)
|
|
170
|
+
path.shift.to_i
|
|
151
171
|
end
|
|
172
|
+
origin_obj = origin_sets.dig(batch_index, origin_index) if origin_index
|
|
152
173
|
|
|
153
174
|
if origin_obj
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
175
|
+
op = ops[batch_index]
|
|
176
|
+
object_path = origin_paths_by_operation.dig(op, origin_index)
|
|
177
|
+
|
|
178
|
+
if object_path
|
|
179
|
+
memo << sanitized_error(err, path: object_path + path)
|
|
180
|
+
next
|
|
181
|
+
end
|
|
158
182
|
end
|
|
183
|
+
|
|
184
|
+
memo << sanitized_error(err, path: path)
|
|
185
|
+
next
|
|
159
186
|
end
|
|
160
187
|
end
|
|
161
188
|
|
|
162
|
-
memo << err
|
|
189
|
+
memo << sanitized_error(err)
|
|
163
190
|
end
|
|
164
|
-
|
|
165
|
-
if pathed_errors_by_op_index_and_object_id.any?
|
|
166
|
-
pathed_errors_by_op_index_and_object_id.each do |op_index, pathed_errors_by_object_id|
|
|
167
|
-
repath_errors!(pathed_errors_by_object_id, ops.dig(op_index, "path"))
|
|
168
|
-
errors_result.push(*pathed_errors_by_object_id.each_value)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
errors_result.tap(&:flatten!)
|
|
173
191
|
end
|
|
174
192
|
|
|
175
193
|
private
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
current_path.push(forward_path.shift)
|
|
181
|
-
scope = root[current_path.last]
|
|
182
|
-
|
|
183
|
-
if forward_path.any? && scope.is_a?(Array)
|
|
184
|
-
scope.each_with_index do |element, index|
|
|
185
|
-
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
|
186
|
-
inner_elements.each do |inner_element|
|
|
187
|
-
current_path << index
|
|
188
|
-
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
|
|
189
|
-
current_path.pop
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
elsif forward_path.any?
|
|
194
|
-
repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
|
|
195
|
-
|
|
196
|
-
elsif scope.is_a?(Array)
|
|
197
|
-
scope.each_with_index do |element, index|
|
|
198
|
-
inner_elements = element.is_a?(Array) ? element.flatten : [element]
|
|
199
|
-
inner_elements.each do |inner_element|
|
|
200
|
-
errors = pathed_errors_by_object_id[inner_element.object_id]
|
|
201
|
-
errors.each { _1["path"] = [*current_path, index, *_1["path"]] } if errors
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
else
|
|
206
|
-
errors = pathed_errors_by_object_id[scope.object_id]
|
|
207
|
-
errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
|
|
195
|
+
def paths_for_origin_set(op, origin_set)
|
|
196
|
+
paths_by_object_id = path_entries(@executor.data, op.path).each_with_object(Hash.new { |h, k| h[k] = [] }) do |(object, path), memo|
|
|
197
|
+
memo[object.object_id] << path
|
|
208
198
|
end
|
|
209
199
|
|
|
210
|
-
|
|
200
|
+
origin_set.map { |origin_obj| paths_by_object_id[origin_obj.object_id].shift }
|
|
211
201
|
end
|
|
212
202
|
end
|
|
213
203
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "executor/path_access"
|
|
4
5
|
require_relative "executor/root_source"
|
|
5
6
|
require_relative "executor/type_resolver_source"
|
|
6
7
|
require_relative "executor/shaper"
|
|
@@ -79,7 +80,7 @@ module GraphQL
|
|
|
79
80
|
|
|
80
81
|
def exec_task(task)
|
|
81
82
|
next_steps = task.load.tap(&:compact!)
|
|
82
|
-
exec!(next_steps)
|
|
83
|
+
exec!(next_steps) unless next_steps.empty?
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
end
|
|
@@ -80,7 +80,7 @@ module GraphQL
|
|
|
80
80
|
|
|
81
81
|
return if files_by_path.none?
|
|
82
82
|
|
|
83
|
-
map = {}
|
|
83
|
+
map = Hash.new { |h, k| h[k] = [] }
|
|
84
84
|
files = files_by_path.values.tap(&:uniq!)
|
|
85
85
|
variables_copy = variables.dup
|
|
86
86
|
|
|
@@ -90,7 +90,6 @@ module GraphQL
|
|
|
90
90
|
path.each_with_index do |key, i|
|
|
91
91
|
if i == path.length - 1
|
|
92
92
|
file_index = files.index(copy[key]).to_s
|
|
93
|
-
map[file_index] ||= []
|
|
94
93
|
map[file_index] << "variables.#{path.join(".")}"
|
|
95
94
|
copy[key] = nil
|
|
96
95
|
elsif orig[key].object_id == copy[key].object_id
|
|
@@ -2,21 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
module GraphQL
|
|
4
4
|
module Stitching
|
|
5
|
-
# Immutable
|
|
5
|
+
# Immutable structures representing a query plan.
|
|
6
6
|
# May serialize to/from JSON.
|
|
7
7
|
class Plan
|
|
8
|
-
Op
|
|
9
|
-
:step
|
|
10
|
-
:after
|
|
11
|
-
:location
|
|
12
|
-
:operation_type
|
|
13
|
-
:selections
|
|
14
|
-
:variables
|
|
15
|
-
:path
|
|
16
|
-
:if_type
|
|
17
|
-
:resolver
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
class Op
|
|
9
|
+
attr_reader :step
|
|
10
|
+
attr_reader :after
|
|
11
|
+
attr_reader :location
|
|
12
|
+
attr_reader :operation_type
|
|
13
|
+
attr_reader :selections
|
|
14
|
+
attr_reader :variables
|
|
15
|
+
attr_reader :path
|
|
16
|
+
attr_reader :if_type
|
|
17
|
+
attr_reader :resolver
|
|
18
|
+
|
|
19
|
+
def initialize(
|
|
20
|
+
step:,
|
|
21
|
+
after:,
|
|
22
|
+
location:,
|
|
23
|
+
operation_type:,
|
|
24
|
+
selections:,
|
|
25
|
+
variables: nil,
|
|
26
|
+
path: nil,
|
|
27
|
+
if_type: nil,
|
|
28
|
+
resolver: nil
|
|
29
|
+
)
|
|
30
|
+
@step = step
|
|
31
|
+
@after = after
|
|
32
|
+
@location = location
|
|
33
|
+
@operation_type = operation_type
|
|
34
|
+
@selections = selections
|
|
35
|
+
@variables = variables
|
|
36
|
+
@path = path
|
|
37
|
+
@if_type = if_type
|
|
38
|
+
@resolver = resolver
|
|
39
|
+
end
|
|
40
|
+
|
|
20
41
|
def as_json
|
|
21
42
|
{
|
|
22
43
|
step: step,
|
|
@@ -30,6 +51,18 @@ module GraphQL
|
|
|
30
51
|
resolver: resolver
|
|
31
52
|
}.tap(&:compact!)
|
|
32
53
|
end
|
|
54
|
+
|
|
55
|
+
def ==(other)
|
|
56
|
+
step == other.step &&
|
|
57
|
+
after == other.after &&
|
|
58
|
+
location == other.location &&
|
|
59
|
+
operation_type == other.operation_type &&
|
|
60
|
+
selections == other.selections &&
|
|
61
|
+
variables == other.variables &&
|
|
62
|
+
path == other.path &&
|
|
63
|
+
if_type == other.if_type &&
|
|
64
|
+
resolver == other.resolver
|
|
65
|
+
end
|
|
33
66
|
end
|
|
34
67
|
|
|
35
68
|
class << self
|