graphql-stitching 0.1.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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