graphql-stitching 0.1.0 → 0.2.2

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.
@@ -6,9 +6,9 @@ module GraphQL
6
6
  SUPERGRAPH_LOCATIONS = [Supergraph::LOCATION].freeze
7
7
  TYPENAME_NODE = GraphQL::Language::Nodes::Field.new(alias: "_STITCH_typename", name: "__typename")
8
8
 
9
- def initialize(supergraph:, document:)
9
+ def initialize(supergraph:, request:)
10
10
  @supergraph = supergraph
11
- @document = document
11
+ @request = request
12
12
  @sequence_key = 0
13
13
  @operations_by_grouping = {}
14
14
  end
@@ -31,44 +31,18 @@ module GraphQL
31
31
 
32
32
  private
33
33
 
34
- def add_operation(location:, parent_type:, selections: nil, insertion_path: [], operation_type: "query", after_key: 0, boundary: nil)
35
- parent_key = @sequence_key += 1
36
- selection_set, variables = if selections&.any?
37
- extract_locale_selections(location, parent_type, selections, insertion_path, parent_key)
38
- end
39
-
40
- grouping = [after_key, location, parent_type.graphql_name, *insertion_path].join("/")
41
-
42
- if op = @operations_by_grouping[grouping]
43
- op.selections += selection_set if selection_set
44
- op.variables.merge!(variables) if variables
45
- return op
46
- end
47
-
48
- type_conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.query && parent_type != @supergraph.schema.mutation
49
-
50
- @operations_by_grouping[grouping] = PlannerOperation.new(
51
- key: parent_key,
52
- after_key: after_key,
53
- location: location,
54
- parent_type: parent_type,
55
- operation_type: operation_type,
56
- insertion_path: insertion_path,
57
- type_condition: type_conditional ? parent_type.graphql_name : nil,
58
- selections: selection_set || [],
59
- variables: variables || {},
60
- boundary: boundary,
61
- )
62
- end
63
-
34
+ # groups root fields by operational strategy:
35
+ # - query immedaitely groups all root fields by location for async resolution
36
+ # - mutation groups sequential root fields by location for serial resolution
64
37
  def build_root_operations
65
- case @document.operation.operation_type
38
+ case @request.operation.operation_type
66
39
  when "query"
67
- # plan steps grouping all fields by location for async execution
68
40
  parent_type = @supergraph.schema.query
69
41
 
70
- selections_by_location = @document.operation.selections.each_with_object({}) do |node, memo|
42
+ selections_by_location = @request.operation.selections.each_with_object({}) do |node, memo|
71
43
  locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
44
+
45
+ # root fields currently just delegate to the last location that defined them; this should probably be smarter
72
46
  memo[locations.last] ||= []
73
47
  memo[locations.last] << node
74
48
  end
@@ -78,20 +52,19 @@ module GraphQL
78
52
  end
79
53
 
80
54
  when "mutation"
81
- # plan steps grouping sequential fields by location for serial execution
82
55
  parent_type = @supergraph.schema.mutation
83
56
  location_groups = []
84
57
 
85
- @document.operation.selections.reduce(nil) do |last_location, node|
86
- location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
87
- if location != last_location
88
- location_groups << {
89
- location: location,
90
- selections: [],
91
- }
58
+ @request.operation.selections.reduce(nil) do |last_location, node|
59
+ # root fields currently just delegate to the last location that defined them; this should probably be smarter
60
+ next_location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
61
+
62
+ if next_location != last_location
63
+ location_groups << { location: next_location, selections: [] }
92
64
  end
65
+
93
66
  location_groups.last[:selections] << node
94
- location
67
+ next_location
95
68
  end
96
69
 
97
70
  location_groups.reduce(0) do |after_key, group|
@@ -109,81 +82,114 @@ module GraphQL
109
82
  end
110
83
  end
111
84
 
112
- def extract_locale_selections(current_location, parent_type, input_selections, insertion_path, after_key)
113
- remote_selections = []
114
- selections_result = []
115
- variables_result = {}
116
- implements_fragments = false
85
+ # adds an operation (data access) to the plan which maps a data selection to an insertion point.
86
+ # note that planned operations are NOT always 1:1 with executed requests, as the executor can
87
+ # frequently batch different insertion points with the same location into a single request.
88
+ def add_operation(
89
+ location:,
90
+ parent_type:,
91
+ selections:,
92
+ insertion_path: [],
93
+ operation_type: "query",
94
+ after_key: 0,
95
+ boundary: nil
96
+ )
97
+ parent_key = @sequence_key += 1
98
+ locale_variables = {}
99
+ locale_selections = if selections.any?
100
+ extract_locale_selections(location, parent_type, selections, insertion_path, parent_key, locale_variables)
101
+ else
102
+ selections
103
+ end
117
104
 
118
- if parent_type.kind.interface?
119
- # fields of a merged interface may not belong to the interface at the local level,
120
- # so these non-local interface fields get expanded into typed fragments for planning
121
- local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
122
- extended_selections = []
123
-
124
- input_selections.reject! do |node|
125
- if node.is_a?(GraphQL::Language::Nodes::Field) && !local_interface_fields.include?(node.name)
126
- extended_selections << node
127
- true
128
- end
129
- end
105
+ # groupings coalesce similar operation parameters into a single operation
106
+ # multiple operations per service may still occur with different insertion points,
107
+ # but those will get query-batched together during execution.
108
+ grouping = String.new
109
+ grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
110
+ grouping = insertion_path.reduce(grouping) do |memo, segment|
111
+ memo << "/" << segment
112
+ end
113
+
114
+ if op = @operations_by_grouping[grouping]
115
+ op.selections.concat(locale_selections)
116
+ op.variables.merge!(locale_variables)
117
+ op
118
+ else
119
+ # concrete types that are not root Query/Mutation report themselves as a type condition
120
+ # executor must check the __typename of loaded objects to see if they match subsequent operations
121
+ # this prevents the executor from taking action on unused fragment selections
122
+ type_conditional = !parent_type.kind.abstract? && parent_type != @supergraph.schema.query && parent_type != @supergraph.schema.mutation
123
+
124
+ @operations_by_grouping[grouping] = PlannerOperation.new(
125
+ key: parent_key,
126
+ after_key: after_key,
127
+ location: location,
128
+ parent_type: parent_type,
129
+ operation_type: operation_type,
130
+ insertion_path: insertion_path,
131
+ type_condition: type_conditional ? parent_type.graphql_name : nil,
132
+ selections: locale_selections,
133
+ variables: locale_variables,
134
+ boundary: boundary,
135
+ )
136
+ end
137
+ end
130
138
 
131
- if extended_selections.any?
132
- possible_types = Util.get_possible_types(@supergraph.schema, parent_type)
133
- possible_types.each do |possible_type|
134
- next if possible_type.kind.abstract? # ignore child interfaces
135
- next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
139
+ # extracts a selection tree that can all be fulfilled through the current planning location.
140
+ # adjoining remote selections will fork new insertion points and extract selections at those locations.
141
+ def extract_locale_selections(current_location, parent_type, input_selections, insertion_path, after_key, locale_variables)
142
+ remote_selections = nil
143
+ locale_selections = []
144
+ implements_fragments = false
136
145
 
137
- type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
138
- input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: extended_selections)
139
- end
140
- end
146
+ if parent_type.kind.interface?
147
+ expand_interface_selections(current_location, parent_type, input_selections)
141
148
  end
142
149
 
143
150
  input_selections.each do |node|
144
151
  case node
145
152
  when GraphQL::Language::Nodes::Field
146
153
  if node.name == "__typename"
147
- selections_result << node
154
+ locale_selections << node
148
155
  next
149
156
  end
150
157
 
151
158
  possible_locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
152
159
  unless possible_locations.include?(current_location)
160
+ remote_selections ||= []
153
161
  remote_selections << node
154
162
  next
155
163
  end
156
164
 
157
- field_type = Util.get_named_type_for_field_node(@supergraph.schema, parent_type, node)
158
-
159
- extract_node_variables!(node, variables_result)
165
+ field_type = Util.named_type_for_field_node(@supergraph.schema, parent_type, node)
166
+ extract_node_variables(node, locale_variables)
160
167
 
161
168
  if Util.is_leaf_type?(field_type)
162
- selections_result << node
169
+ locale_selections << node
163
170
  else
164
- expanded_path = [*insertion_path, node.alias || node.name]
165
- selection_set, variables = extract_locale_selections(current_location, field_type, node.selections, expanded_path, after_key)
166
- selections_result << node.merge(selections: selection_set)
167
- variables_result.merge!(variables)
171
+ insertion_path.push(node.alias || node.name)
172
+ selection_set = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key, locale_variables)
173
+ insertion_path.pop
174
+
175
+ locale_selections << node.merge(selections: selection_set)
168
176
  end
169
177
 
170
178
  when GraphQL::Language::Nodes::InlineFragment
171
179
  next unless @supergraph.locations_by_type[node.type.name].include?(current_location)
172
180
 
173
181
  fragment_type = @supergraph.schema.types[node.type.name]
174
- selection_set, variables = extract_locale_selections(current_location, fragment_type, node.selections, insertion_path, after_key)
175
- selections_result << node.merge(selections: selection_set)
176
- variables_result.merge!(variables)
182
+ selection_set = extract_locale_selections(current_location, fragment_type, node.selections, insertion_path, after_key, locale_variables)
183
+ locale_selections << node.merge(selections: selection_set)
177
184
  implements_fragments = true
178
185
 
179
186
  when GraphQL::Language::Nodes::FragmentSpread
180
- fragment = @document.fragment_definitions[node.name]
187
+ fragment = @request.fragment_definitions[node.name]
181
188
  next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
182
189
 
183
190
  fragment_type = @supergraph.schema.types[fragment.type.name]
184
- selection_set, variables = extract_locale_selections(current_location, fragment_type, fragment.selections, insertion_path, after_key)
185
- selections_result << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
186
- variables_result.merge!(variables)
191
+ selection_set = extract_locale_selections(current_location, fragment_type, fragment.selections, insertion_path, after_key, locale_variables)
192
+ locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
187
193
  implements_fragments = true
188
194
 
189
195
  else
@@ -191,25 +197,35 @@ module GraphQL
191
197
  end
192
198
  end
193
199
 
194
- if remote_selections.any?
195
- selection_set = build_child_operations(current_location, parent_type, remote_selections, insertion_path, after_key)
196
- selections_result.concat(selection_set)
200
+ if remote_selections
201
+ delegate_remote_selections(
202
+ current_location,
203
+ parent_type,
204
+ locale_selections,
205
+ remote_selections,
206
+ insertion_path,
207
+ after_key
208
+ )
197
209
  end
198
210
 
211
+ # always include a __typename on abstracts and scopes that implement fragments
212
+ # this provides type information to inspect while shaping the final result
199
213
  if parent_type.kind.abstract? || implements_fragments
200
- selections_result << TYPENAME_NODE
214
+ locale_selections << TYPENAME_NODE
201
215
  end
202
216
 
203
- return selections_result, variables_result
217
+ locale_selections
204
218
  end
205
219
 
206
- def build_child_operations(current_location, parent_type, input_selections, insertion_path, after_key)
207
- parent_selections_result = []
220
+ # distributes remote selections across locations,
221
+ # while spawning new operations for each new fulfillment.
222
+ def delegate_remote_selections(current_location, parent_type, locale_selections, remote_selections, insertion_path, after_key)
223
+ possible_locations_by_field = @supergraph.locations_by_type_and_field[parent_type.graphql_name]
208
224
  selections_by_location = {}
209
225
 
210
226
  # distribute unique fields among required locations
211
- input_selections.reject! do |node|
212
- possible_locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name]
227
+ remote_selections.reject! do |node|
228
+ possible_locations = possible_locations_by_field[node.name]
213
229
  if possible_locations.length == 1
214
230
  selections_by_location[possible_locations.first] ||= []
215
231
  selections_by_location[possible_locations.first] << node
@@ -217,31 +233,35 @@ module GraphQL
217
233
  end
218
234
  end
219
235
 
220
- # distribute non-unique fields among available locations, preferring used locations
221
- if input_selections.any?
222
- # weight locations by number of needed fields available, prefer greater availability
223
- location_weights = input_selections.each_with_object({}) do |node, memo|
224
- possible_locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name]
225
- possible_locations.each do |location|
226
- memo[location] ||= 0
227
- memo[location] += 1
236
+ # distribute non-unique fields among available locations, preferring locations already used
237
+ if remote_selections.any?
238
+ # weight locations by number of required fields available, preferring greater availability
239
+ location_weights = if remote_selections.length > 1
240
+ remote_selections.each_with_object({}) do |node, memo|
241
+ possible_locations = possible_locations_by_field[node.name]
242
+ possible_locations.each do |location|
243
+ memo[location] ||= 0
244
+ memo[location] += 1
245
+ end
228
246
  end
247
+ else
248
+ GraphQL::Stitching::EMPTY_OBJECT
229
249
  end
230
250
 
231
- input_selections.each do |node|
232
- possible_locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name]
233
-
234
- perfect_location_score = input_selections.length
251
+ remote_selections.each do |node|
252
+ possible_locations = possible_locations_by_field[node.name]
235
253
  preferred_location_score = 0
236
- preferred_location = possible_locations.reduce(possible_locations.first) do |current_loc, candidate_loc|
237
- score = selections_by_location[location] ? perfect_location_score : 0
238
- score += location_weights.fetch(candidate_loc, 0)
254
+
255
+ # hill climbing selects highest scoring locations to use
256
+ preferred_location = possible_locations.reduce(possible_locations.first) do |best_location, possible_location|
257
+ score = selections_by_location[location] ? remote_selections.length : 0
258
+ score += location_weights.fetch(possible_location, 0)
239
259
 
240
260
  if score > preferred_location_score
241
261
  preferred_location_score = score
242
- candidate_loc
262
+ possible_location
243
263
  else
244
- current_loc
264
+ best_location
245
265
  end
246
266
  end
247
267
 
@@ -251,70 +271,103 @@ module GraphQL
251
271
  end
252
272
 
253
273
  routes = @supergraph.route_type_to_locations(parent_type.graphql_name, current_location, selections_by_location.keys)
254
- routes.values.each_with_object({}) do |route, memo|
274
+ routes.values.each_with_object({}) do |route, ops_by_location|
255
275
  route.reduce(nil) do |parent_op, boundary|
256
276
  location = boundary["location"]
257
- next memo[location] if memo[location]
277
+ new_operation = false
258
278
 
259
- child_op = memo[location] = add_operation(
260
- location: location,
261
- selections: selections_by_location[location],
262
- parent_type: parent_type,
263
- insertion_path: insertion_path,
264
- boundary: boundary,
265
- after_key: after_key,
266
- )
267
-
268
- foreign_key_node = GraphQL::Language::Nodes::Field.new(
269
- alias: "_STITCH_#{boundary["selection"]}",
270
- name: boundary["selection"]
271
- )
272
-
273
- if parent_op
274
- parent_op.selections << foreign_key_node << TYPENAME_NODE
275
- else
276
- parent_selections_result << foreign_key_node << TYPENAME_NODE
279
+ unless op = ops_by_location[location]
280
+ new_operation = true
281
+ op = ops_by_location[location] = add_operation(
282
+ location: location,
283
+ # routing locations added as intermediaries have no initial selections,
284
+ # but will be given foreign keys by subsequent operations
285
+ selections: selections_by_location[location] || [],
286
+ parent_type: parent_type,
287
+ insertion_path: insertion_path.dup,
288
+ boundary: boundary,
289
+ after_key: after_key,
290
+ )
291
+ end
292
+
293
+ foreign_key = "_STITCH_#{boundary["selection"]}"
294
+ parent_selections = parent_op ? parent_op.selections : locale_selections
295
+
296
+ if new_operation || parent_selections.none? { _1.is_a?(GraphQL::Language::Nodes::Field) && _1.alias == foreign_key }
297
+ foreign_key_node = GraphQL::Language::Nodes::Field.new(alias: foreign_key, name: boundary["selection"])
298
+ parent_selections << foreign_key_node << TYPENAME_NODE
277
299
  end
278
300
 
279
- child_op
301
+ op
280
302
  end
281
303
  end
282
-
283
- parent_selections_result
284
304
  end
285
305
 
286
- def extract_node_variables!(node_with_args, variables={})
287
- node_with_args.arguments.each_with_object(variables) do |argument, memo|
306
+ # extracts variable definitions used by a node
307
+ # (each operation tracks the specific variables used in its tree)
308
+ def extract_node_variables(node_with_args, variable_definitions)
309
+ node_with_args.arguments.each do |argument|
288
310
  case argument.value
289
311
  when GraphQL::Language::Nodes::InputObject
290
- extract_node_variables!(argument.value, memo)
312
+ extract_node_variables(argument.value, variable_definitions)
291
313
  when GraphQL::Language::Nodes::VariableIdentifier
292
- memo[argument.value.name] ||= @document.variable_definitions[argument.value.name]
314
+ variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
315
+ end
316
+ end
317
+
318
+ if node_with_args.respond_to?(:directives)
319
+ node_with_args.directives.each do |directive|
320
+ extract_node_variables(directive, variable_definitions)
321
+ end
322
+ end
323
+ end
324
+
325
+ # fields of a merged interface may not belong to the interface at the local level,
326
+ # so any non-local interface fields get expanded into typed fragments before planning
327
+ def expand_interface_selections(current_location, parent_type, input_selections)
328
+ local_interface_fields = @supergraph.fields_by_type_and_location[parent_type.graphql_name][current_location]
329
+
330
+ expanded_selections = nil
331
+ input_selections.reject! do |node|
332
+ if node.is_a?(GraphQL::Language::Nodes::Field) && !local_interface_fields.include?(node.name)
333
+ expanded_selections ||= []
334
+ expanded_selections << node
335
+ true
336
+ end
337
+ end
338
+
339
+ if expanded_selections
340
+ @supergraph.schema.possible_types(parent_type).each do |possible_type|
341
+ next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)
342
+
343
+ type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
344
+ input_selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
293
345
  end
294
346
  end
295
347
  end
296
348
 
297
349
  # expand concrete type selections into typed fragments when sending to abstract boundaries
350
+ # this shifts all loose selection fields into a wrapping concrete type fragment
298
351
  def expand_abstract_boundaries
299
352
  @operations_by_grouping.each do |_grouping, op|
300
353
  next unless op.boundary
301
354
 
302
- boundary_type = @supergraph.schema.get_type(op.boundary["type_name"])
355
+ boundary_type = @supergraph.schema.types[op.boundary["type_name"]]
303
356
  next unless boundary_type.kind.abstract?
357
+ next if boundary_type == op.parent_type
304
358
 
305
- unless op.parent_type == boundary_type
306
- to_typed_selections = []
307
- op.selections.reject! do |node|
308
- if node.is_a?(GraphQL::Language::Nodes::Field)
309
- to_typed_selections << node
310
- true
311
- end
359
+ expanded_selections = nil
360
+ op.selections.reject! do |node|
361
+ if node.is_a?(GraphQL::Language::Nodes::Field)
362
+ expanded_selections ||= []
363
+ expanded_selections << node
364
+ true
312
365
  end
366
+ end
313
367
 
314
- if to_typed_selections.any?
315
- type_name = GraphQL::Language::Nodes::TypeName.new(name: op.parent_type.graphql_name)
316
- op.selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: to_typed_selections)
317
- end
368
+ if expanded_selections
369
+ type_name = GraphQL::Language::Nodes::TypeName.new(name: op.parent_type.graphql_name)
370
+ op.selections << GraphQL::Language::Nodes::InlineFragment.new(type: type_name, selections: expanded_selections)
318
371
  end
319
372
  end
320
373
  end
@@ -9,14 +9,14 @@ module GraphQL
9
9
  class RemoteClient
10
10
  def initialize(url:, headers:{})
11
11
  @url = url
12
- @headers = headers
12
+ @headers = { "Content-Type" => "application/json" }.merge!(headers)
13
13
  end
14
14
 
15
- def call(location, document, variables)
15
+ def call(_location, document, variables, _context)
16
16
  response = Net::HTTP.post(
17
17
  URI(@url),
18
- { "query" => document, "variables" => variables }.to_json,
19
- { "Content-Type" => "application/json" }.merge!(@headers)
18
+ JSON.generate({ "query" => document, "variables" => variables }),
19
+ @headers,
20
20
  )
21
21
  JSON.parse(response.body)
22
22
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Stitching
5
+ class Request
6
+ SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
7
+
8
+ class ApplyRuntimeDirectives < GraphQL::Language::Visitor
9
+ def initialize(document, variables)
10
+ @changed = false
11
+ @variables = variables
12
+ super(document)
13
+ end
14
+
15
+ def changed?
16
+ @changed
17
+ end
18
+
19
+ def on_field(node, parent)
20
+ delete_node = false
21
+ filtered_directives = if node.directives.any?
22
+ node.directives.select do |directive|
23
+ if directive.name == "skip"
24
+ delete_node = assess_argument_value(directive.arguments.first)
25
+ false
26
+ elsif directive.name == "include"
27
+ delete_node = !assess_argument_value(directive.arguments.first)
28
+ false
29
+ else
30
+ true
31
+ end
32
+ end
33
+ end
34
+
35
+ if delete_node
36
+ @changed = true
37
+ super(DELETE_NODE, parent)
38
+ elsif filtered_directives && filtered_directives.length != node.directives.length
39
+ @changed = true
40
+ super(node.merge(directives: filtered_directives), parent)
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def assess_argument_value(arg)
49
+ if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
50
+ return @variables[arg.value.name]
51
+ end
52
+ arg.value
53
+ end
54
+ end
55
+
56
+ attr_reader :document, :variables, :operation_name, :context
57
+
58
+ def initialize(document, operation_name: nil, variables: nil, context: nil)
59
+ @may_contain_runtime_directives = true
60
+
61
+ @document = if document.is_a?(String)
62
+ @may_contain_runtime_directives = document.include?("@")
63
+ GraphQL.parse(document)
64
+ else
65
+ document
66
+ end
67
+
68
+ @operation_name = operation_name
69
+ @variables = variables || {}
70
+ @context = context || GraphQL::Stitching::EMPTY_OBJECT
71
+ end
72
+
73
+ def string
74
+ @string ||= @document.to_query_string
75
+ end
76
+
77
+ def digest
78
+ @digest ||= Digest::SHA2.hexdigest(string)
79
+ end
80
+
81
+ def operation
82
+ @operation ||= begin
83
+ operation_defs = @document.definitions.select do |d|
84
+ next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
85
+ next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
86
+ @operation_name ? d.name == @operation_name : true
87
+ end
88
+
89
+ if operation_defs.length < 1
90
+ raise GraphQL::ExecutionError, "Invalid root operation."
91
+ elsif operation_defs.length > 1
92
+ raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
93
+ end
94
+
95
+ operation_defs.first
96
+ end
97
+ end
98
+
99
+ def variable_definitions
100
+ @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
101
+ memo[v.name] = v.type
102
+ end
103
+ end
104
+
105
+ def fragment_definitions
106
+ @fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
107
+ memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
108
+ end
109
+ end
110
+
111
+ def prepare!
112
+ operation.variables.each do |v|
113
+ @variables[v.name] ||= v.default_value
114
+ end
115
+
116
+ if @may_contain_runtime_directives
117
+ visitor = ApplyRuntimeDirectives.new(@document, @variables)
118
+ @document = visitor.visit
119
+
120
+ if visitor.changed?
121
+ @string = nil
122
+ @digest = nil
123
+ @operation = nil
124
+ @variable_definitions = nil
125
+ @fragment_definitions = nil
126
+ end
127
+ end
128
+
129
+ self
130
+ end
131
+ end
132
+ end
133
+ end