graphql-stitching 0.0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,16 +7,17 @@ module GraphQL
7
7
  class Executor
8
8
 
9
9
  class RootSource < GraphQL::Dataloader::Source
10
- def initialize(executor)
10
+ def initialize(executor, location)
11
11
  @executor = executor
12
+ @location = location
12
13
  end
13
14
 
14
15
  def fetch(ops)
15
16
  op = ops.first # There should only ever be one per location at a time
16
17
 
17
18
  query_document = build_query(op)
18
- query_variables = @executor.variables.slice(*op["variables"].keys)
19
- result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables)
19
+ query_variables = @executor.request.variables.slice(*op["variables"].keys)
20
+ result = @executor.supergraph.execute_at_location(op["location"], query_document, query_variables, @executor.request.context)
20
21
  @executor.query_count += 1
21
22
 
22
23
  @executor.data.merge!(result["data"]) if result["data"]
@@ -24,7 +25,8 @@ module GraphQL
24
25
  result["errors"].each { _1.delete("locations") }
25
26
  @executor.errors.concat(result["errors"])
26
27
  end
27
- op["key"]
28
+
29
+ ops.map { op["key"] }
28
30
  end
29
31
 
30
32
  def build_query(op)
@@ -61,8 +63,8 @@ module GraphQL
61
63
 
62
64
  if origin_sets_by_operation.any?
63
65
  query_document, variable_names = build_query(origin_sets_by_operation)
64
- variables = @executor.variables.slice(*variable_names)
65
- raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables)
66
+ variables = @executor.request.variables.slice(*variable_names)
67
+ raw_result = @executor.supergraph.execute_at_location(@location, query_document, variables, @executor.request.context)
66
68
  @executor.query_count += 1
67
69
 
68
70
  merge_results!(origin_sets_by_operation, raw_result.dig("data"))
@@ -165,23 +167,26 @@ module GraphQL
165
167
 
166
168
  private
167
169
 
168
- # traverses forward through origin data, expanding arrays to follow all paths
170
+ # traverse forward through origin data, expanding arrays to follow all paths
169
171
  # any errors found for an origin object_id have their path prefixed by the object path
170
172
  def repath_errors!(pathed_errors_by_object_id, forward_path, current_path=[], root=@executor.data)
171
- current_path << forward_path.first
172
- forward_path = forward_path[1..-1]
173
+ current_path.push(forward_path.shift)
173
174
  scope = root[current_path.last]
174
175
 
175
176
  if forward_path.any? && scope.is_a?(Array)
176
177
  scope.each_with_index do |element, index|
177
178
  inner_elements = element.is_a?(Array) ? element.flatten : [element]
178
179
  inner_elements.each do |inner_element|
179
- repath_errors!(pathed_errors_by_object_id, forward_path, [*current_path, index], inner_element)
180
+ current_path << index
181
+ repath_errors!(pathed_errors_by_object_id, forward_path, current_path, inner_element)
182
+ current_path.pop
180
183
  end
181
184
  end
182
185
 
183
186
  elsif forward_path.any?
184
- repath_errors!(pathed_errors_by_object_id, forward_path, [*current_path, index], scope)
187
+ current_path << index
188
+ repath_errors!(pathed_errors_by_object_id, forward_path, current_path, scope)
189
+ current_path.pop
185
190
 
186
191
  elsif scope.is_a?(Array)
187
192
  scope.each_with_index do |element, index|
@@ -196,56 +201,72 @@ module GraphQL
196
201
  errors = pathed_errors_by_object_id[scope.object_id]
197
202
  errors.each { _1["path"] = [*current_path, *_1["path"]] } if errors
198
203
  end
204
+
205
+ forward_path.unshift(current_path.pop)
199
206
  end
200
207
  end
201
208
 
202
- attr_reader :supergraph, :data, :errors, :variables
209
+ attr_reader :supergraph, :request, :data, :errors
203
210
  attr_accessor :query_count
204
211
 
205
- def initialize(supergraph:, plan:, variables: {}, nonblocking: false)
212
+ def initialize(supergraph:, request:, plan:, nonblocking: false)
206
213
  @supergraph = supergraph
207
- @variables = variables
214
+ @request = request
208
215
  @queue = plan["ops"]
209
216
  @data = {}
210
217
  @errors = []
211
218
  @query_count = 0
219
+ @exec_cycles = 0
212
220
  @dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
213
221
  end
214
222
 
215
- def perform(document=nil)
223
+ def perform(raw: false)
216
224
  exec!
217
-
218
225
  result = {}
219
- result["data"] = @data if @data && @data.length > 0
220
- result["errors"] = @errors if @errors.length > 0
221
226
 
222
- result if document.nil?
227
+ if @data && @data.length > 0
228
+ result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(
229
+ schema: @supergraph.schema,
230
+ request: @request,
231
+ ).perform!(@data)
232
+ end
223
233
 
224
- GraphQL::Stitching::Shaper.new(supergraph: @supergraph, document: document, raw: result).perform!
234
+ if @errors.length > 0
235
+ result["errors"] = @errors
236
+ end
237
+
238
+ result
225
239
  end
226
240
 
227
241
  private
228
242
 
229
243
  def exec!(after_keys = [0])
244
+ if @exec_cycles > @queue.length
245
+ # sanity check... if we've exceeded queue size, then something went wrong.
246
+ raise StitchingError, "Too many execution requests attempted."
247
+ end
248
+
230
249
  @dataloader.append_job do
231
- requests = @queue
250
+ tasks = @queue
232
251
  .select { after_keys.include?(_1["after_key"]) }
233
- .group_by { _1["location"] }
234
- .map do |location, ops|
235
- if ops.first["after_key"].zero?
236
- @dataloader.with(RootSource, self).request_all(ops)
252
+ .group_by { [_1["location"], _1["boundary"].nil?] }
253
+ .map do |(location, root_source), ops|
254
+ if root_source
255
+ @dataloader.with(RootSource, self, location).request_all(ops)
237
256
  else
238
257
  @dataloader.with(BoundarySource, self, location).request_all(ops)
239
258
  end
240
259
  end
241
260
 
242
- requests.each(&method(:exec_request))
261
+ tasks.each(&method(:exec_task))
243
262
  end
263
+
264
+ @exec_cycles += 1
244
265
  @dataloader.run
245
266
  end
246
267
 
247
- def exec_request(request)
248
- next_keys = request.load
268
+ def exec_task(task)
269
+ next_keys = task.load
249
270
  next_keys.compact!
250
271
  exec!(next_keys) if next_keys.any?
251
272
  end
@@ -7,8 +7,6 @@ module GraphQL
7
7
  class Gateway
8
8
  class GatewayError < StitchingError; end
9
9
 
10
- EMPTY_CONTEXT = {}.freeze
11
-
12
10
  attr_reader :supergraph
13
11
 
14
12
  def initialize(locations: nil, supergraph: nil)
@@ -25,29 +23,36 @@ module GraphQL
25
23
  end
26
24
  end
27
25
 
28
- def execute(query:, variables: nil, operation_name: nil, context: EMPTY_CONTEXT, validate: true)
29
- document = GraphQL::Stitching::Document.new(query, operation_name: operation_name)
26
+ def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
27
+ request = GraphQL::Stitching::Request.new(
28
+ query,
29
+ operation_name: operation_name,
30
+ variables: variables,
31
+ context: context,
32
+ )
30
33
 
31
34
  if validate
32
- validation_errors = @supergraph.schema.validate(document.ast)
35
+ validation_errors = @supergraph.schema.validate(request.document)
33
36
  return error_result(validation_errors) if validation_errors.any?
34
37
  end
35
38
 
39
+ request.prepare!
40
+
36
41
  begin
37
- plan = fetch_plan(document, context) do
42
+ plan = fetch_plan(request) do
38
43
  GraphQL::Stitching::Planner.new(
39
44
  supergraph: @supergraph,
40
- document: document,
45
+ request: request,
41
46
  ).perform.to_h
42
47
  end
43
48
 
44
49
  GraphQL::Stitching::Executor.new(
45
50
  supergraph: @supergraph,
51
+ request: request,
46
52
  plan: plan,
47
- variables: variables || {},
48
- ).perform(document)
53
+ ).perform
49
54
  rescue StandardError => e
50
- custom_message = @on_error.call(e, context) if @on_error
55
+ custom_message = @on_error.call(e, request.context) if @on_error
51
56
  error_result([{ "message" => custom_message || "An unexpected error occured." }])
52
57
  end
53
58
  end
@@ -91,16 +96,16 @@ module GraphQL
91
96
  supergraph
92
97
  end
93
98
 
94
- def fetch_plan(document, context)
99
+ def fetch_plan(request)
95
100
  if @on_cache_read
96
- cached_plan = @on_cache_read.call(document.digest, context)
101
+ cached_plan = @on_cache_read.call(request.digest, request.context)
97
102
  return JSON.parse(cached_plan) if cached_plan
98
103
  end
99
104
 
100
105
  plan_json = yield
101
106
 
102
107
  if @on_cache_write
103
- @on_cache_write.call(document.digest, JSON.generate(plan_json), context)
108
+ @on_cache_write.call(request.digest, JSON.generate(plan_json), request.context)
104
109
  end
105
110
 
106
111
  plan_json
@@ -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
@@ -37,7 +37,11 @@ module GraphQL
37
37
  extract_locale_selections(location, parent_type, selections, insertion_path, parent_key)
38
38
  end
39
39
 
40
- grouping = [after_key, location, parent_type.graphql_name, *insertion_path].join("/")
40
+ grouping = String.new
41
+ grouping << after_key.to_s << "/" << location << "/" << parent_type.graphql_name
42
+ grouping = insertion_path.reduce(grouping) do |memo, segment|
43
+ memo << "/" << segment
44
+ end
41
45
 
42
46
  if op = @operations_by_grouping[grouping]
43
47
  op.selections += selection_set if selection_set
@@ -62,12 +66,12 @@ module GraphQL
62
66
  end
63
67
 
64
68
  def build_root_operations
65
- case @document.operation.operation_type
69
+ case @request.operation.operation_type
66
70
  when "query"
67
71
  # plan steps grouping all fields by location for async execution
68
72
  parent_type = @supergraph.schema.query
69
73
 
70
- selections_by_location = @document.operation.selections.each_with_object({}) do |node, memo|
74
+ selections_by_location = @request.operation.selections.each_with_object({}) do |node, memo|
71
75
  locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
72
76
  memo[locations.last] ||= []
73
77
  memo[locations.last] << node
@@ -82,7 +86,7 @@ module GraphQL
82
86
  parent_type = @supergraph.schema.mutation
83
87
  location_groups = []
84
88
 
85
- @document.operation.selections.reduce(nil) do |last_location, node|
89
+ @request.operation.selections.reduce(nil) do |last_location, node|
86
90
  location = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name].last
87
91
  if location != last_location
88
92
  location_groups << {
@@ -161,8 +165,10 @@ module GraphQL
161
165
  if Util.is_leaf_type?(field_type)
162
166
  selections_result << node
163
167
  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)
168
+ insertion_path.push(node.alias || node.name)
169
+ selection_set, variables = extract_locale_selections(current_location, field_type, node.selections, insertion_path, after_key)
170
+ insertion_path.pop
171
+
166
172
  selections_result << node.merge(selections: selection_set)
167
173
  variables_result.merge!(variables)
168
174
  end
@@ -177,7 +183,7 @@ module GraphQL
177
183
  implements_fragments = true
178
184
 
179
185
  when GraphQL::Language::Nodes::FragmentSpread
180
- fragment = @document.fragment_definitions[node.name]
186
+ fragment = @request.fragment_definitions[node.name]
181
187
  next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
182
188
 
183
189
  fragment_type = @supergraph.schema.types[fragment.type.name]
@@ -260,7 +266,7 @@ module GraphQL
260
266
  location: location,
261
267
  selections: selections_by_location[location],
262
268
  parent_type: parent_type,
263
- insertion_path: insertion_path,
269
+ insertion_path: insertion_path.dup,
264
270
  boundary: boundary,
265
271
  after_key: after_key,
266
272
  )
@@ -289,7 +295,7 @@ module GraphQL
289
295
  when GraphQL::Language::Nodes::InputObject
290
296
  extract_node_variables!(argument.value, memo)
291
297
  when GraphQL::Language::Nodes::VariableIdentifier
292
- memo[argument.value.name] ||= @document.variable_definitions[argument.value.name]
298
+ memo[argument.value.name] ||= @request.variable_definitions[argument.value.name]
293
299
  end
294
300
  end
295
301
  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
+ EMPTY_CONTEXT = {}.freeze
8
+
9
+ class ApplyRuntimeDirectives < GraphQL::Language::Visitor
10
+ def initialize(document, variables)
11
+ @changed = false
12
+ @variables = variables
13
+ super(document)
14
+ end
15
+
16
+ def changed?
17
+ @changed
18
+ end
19
+
20
+ def on_field(node, parent)
21
+ delete_node = false
22
+ filtered_directives = if node.directives.any?
23
+ node.directives.select do |directive|
24
+ if directive.name == "skip"
25
+ delete_node = assess_argument_value(directive.arguments.first)
26
+ false
27
+ elsif directive.name == "include"
28
+ delete_node = !assess_argument_value(directive.arguments.first)
29
+ false
30
+ else
31
+ true
32
+ end
33
+ end
34
+ end
35
+
36
+ if delete_node
37
+ @changed = true
38
+ super(DELETE_NODE, parent)
39
+ elsif filtered_directives && filtered_directives.length != node.directives.length
40
+ @changed = true
41
+ super(node.merge(directives: filtered_directives), parent)
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def assess_argument_value(arg)
50
+ if arg.value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
51
+ return @variables[arg.value.name]
52
+ end
53
+ arg.value
54
+ end
55
+ end
56
+
57
+ attr_reader :document, :variables, :operation_name, :context
58
+
59
+ def initialize(document, operation_name: nil, variables: nil, context: nil)
60
+ @may_contain_runtime_directives = true
61
+
62
+ @document = if document.is_a?(String)
63
+ @may_contain_runtime_directives = document.include?("@")
64
+ GraphQL.parse(document)
65
+ else
66
+ document
67
+ end
68
+
69
+ @operation_name = operation_name
70
+ @variables = variables || {}
71
+ @context = context || EMPTY_CONTEXT
72
+ end
73
+
74
+ def string
75
+ @string ||= @document.to_query_string
76
+ end
77
+
78
+ def digest
79
+ @digest ||= Digest::SHA2.hexdigest(string)
80
+ end
81
+
82
+ def operation
83
+ @operation ||= begin
84
+ operation_defs = @document.definitions.select do |d|
85
+ next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
86
+ next unless SUPPORTED_OPERATIONS.include?(d.operation_type)
87
+ @operation_name ? d.name == @operation_name : true
88
+ end
89
+
90
+ if operation_defs.length < 1
91
+ raise GraphQL::ExecutionError, "Invalid root operation."
92
+ elsif operation_defs.length > 1
93
+ raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
94
+ end
95
+
96
+ operation_defs.first
97
+ end
98
+ end
99
+
100
+ def variable_definitions
101
+ @variable_definitions ||= operation.variables.each_with_object({}) do |v, memo|
102
+ memo[v.name] = v.type
103
+ end
104
+ end
105
+
106
+ def fragment_definitions
107
+ @fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
108
+ memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
109
+ end
110
+ end
111
+
112
+ def prepare!
113
+ operation.variables.each do |v|
114
+ @variables[v.name] ||= v.default_value
115
+ end
116
+
117
+ return self unless @may_contain_runtime_directives
118
+
119
+ visitor = ApplyRuntimeDirectives.new(@document, @variables)
120
+ @document = visitor.visit
121
+
122
+ if visitor.changed?
123
+ @string = nil
124
+ @digest = nil
125
+ @operation = nil
126
+ @variable_definitions = nil
127
+ @fragment_definitions = nil
128
+ end
129
+ self
130
+ end
131
+ end
132
+ end
133
+ end
@@ -4,88 +4,92 @@
4
4
  module GraphQL
5
5
  module Stitching
6
6
  class Shaper
7
- def initialize(supergraph:, document:, raw:)
8
- @supergraph = supergraph
9
- @document = document
10
- @result = raw
11
- @errors = []
7
+ def initialize(schema:, request:)
8
+ @schema = schema
9
+ @request = request
12
10
  end
13
11
 
14
- def perform!
15
- if @result.key?("data") && ! @result["data"].empty?
16
- munge_entry(@result["data"], @document.operation.selections, @supergraph.schema.query)
17
- # hate doing a second pass, but cannot remove _STITCH_ fields until the fragements are processed
18
- clean_entry(@result["data"])
19
- end
20
-
21
- if @errors.length > 0
22
- @result = [] unless @result.key?("errors")
23
- @result["errors"] += @errors
24
- end
25
-
26
- @result
12
+ def perform!(raw)
13
+ root_type = @schema.public_send(@request.operation.operation_type)
14
+ resolve_object_scope(raw, root_type, @request.operation.selections)
27
15
  end
28
16
 
29
17
  private
30
18
 
31
- def munge_entry(entry, selections, parent_type)
19
+ def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
20
+ return nil if raw_object.nil?
21
+
22
+ typename ||= raw_object["_STITCH_typename"]
23
+ raw_object.reject! { |k, _v| k.start_with?("_STITCH_") }
24
+
32
25
  selections.each do |node|
33
26
  case node
34
27
  when GraphQL::Language::Nodes::Field
35
- next if node.respond_to?(:name) && node&.name == "__typename"
28
+ next if node.name.start_with?("__")
36
29
 
37
- munge_field(entry, node, parent_type)
30
+ field_name = node.alias || node.name
31
+ node_type = parent_type.fields[node.name].type
32
+ named_type = Util.get_named_type_for_field_node(@schema, parent_type, node)
33
+
34
+ raw_object[field_name] = if node_type.list?
35
+ resolve_list_scope(raw_object[field_name], Util.unwrap_non_null(node_type), node.selections)
36
+ elsif Util.is_leaf_type?(named_type)
37
+ raw_object[field_name]
38
+ else
39
+ resolve_object_scope(raw_object[field_name], named_type, node.selections)
40
+ end
41
+
42
+ return nil if raw_object[field_name].nil? && node_type.non_null?
38
43
 
39
44
  when GraphQL::Language::Nodes::InlineFragment
40
- next unless entry["_STITCH_typename"] == node.type.name
41
- fragment_type = @supergraph.schema.types[node.type.name]
42
- munge_entry(entry, node.selections, fragment_type)
45
+ next unless typename == node.type.name
46
+ fragment_type = @schema.types[node.type.name]
47
+ result = resolve_object_scope(raw_object, fragment_type, node.selections, typename)
48
+ return nil if result.nil?
43
49
 
44
50
  when GraphQL::Language::Nodes::FragmentSpread
45
- next unless entry["_STITCH_typename"] == node.name
46
- fragment = @document.fragment_definitions[node.name]
47
- fragment_type = @supergraph.schema.types[node.name]
48
- munge_entry(entry, fragment.selections, fragment_type)
51
+ fragment = @request.fragment_definitions[node.name]
52
+ fragment_type = @schema.types[fragment.type.name]
53
+ next unless typename == fragment_type.graphql_name
54
+
55
+ result = resolve_object_scope(raw_object, fragment_type, fragment.selections, typename)
56
+ return nil if result.nil?
57
+
49
58
  else
50
59
  raise "Unexpected node of type #{node.class.name} in selection set."
51
60
  end
52
61
  end
62
+
63
+ raw_object
53
64
  end
54
65
 
55
- def munge_field(entry, node, parent_type)
56
- field_identifier = (node.alias || node.name)
57
- node_type = Util.get_named_type_for_field_node(@supergraph.schema, parent_type, node)
66
+ def resolve_list_scope(raw_list, current_node_type, selections)
67
+ return nil if raw_list.nil?
58
68
 
59
- if entry.nil?
60
- return # TODO bubble up error if not nullable
61
- end
69
+ next_node_type = Util.unwrap_non_null(current_node_type).of_type
70
+ named_type = Util.get_named_type(next_node_type)
71
+ contains_null = false
62
72
 
63
- child_entry = entry[field_identifier]
64
- if child_entry.nil?
65
- entry[field_identifier] = nil
66
- elsif child_entry.is_a? Array
67
- child_entry.each do |raw_item|
68
- munge_entry(raw_item, node.selections, node_type)
73
+ resolved_list = raw_list.map! do |raw_list_element|
74
+ result = if next_node_type.list?
75
+ resolve_list_scope(raw_list_element, next_node_type, selections)
76
+ elsif Util.is_leaf_type?(named_type)
77
+ raw_list_element
78
+ else
79
+ resolve_object_scope(raw_list_element, named_type, selections)
69
80
  end
70
- elsif ! Util.is_leaf_type?(node_type)
71
- munge_entry(child_entry, node.selections, node_type)
72
- end
73
- end
74
81
 
75
- def clean_entry(entry)
76
- return if entry.nil?
77
-
78
- entry.each do |key, value|
79
- if key.start_with? "_STITCH_"
80
- entry.delete(key)
81
- elsif value.is_a?(Array)
82
- value.each do |item|
83
- clean_entry(item)
84
- end
85
- elsif value.is_a?(Hash)
86
- clean_entry(value)
82
+ if result.nil?
83
+ contains_null = true
84
+ return nil if current_node_type.non_null?
87
85
  end
86
+
87
+ result
88
88
  end
89
+
90
+ return nil if contains_null && next_node_type.non_null?
91
+
92
+ resolved_list
89
93
  end
90
94
  end
91
95
  end
@@ -57,27 +57,28 @@ module GraphQL
57
57
  def assign_executable(location, executable = nil, &block)
58
58
  executable ||= block
59
59
  unless executable.is_a?(Class) && executable <= GraphQL::Schema
60
- raise "A client or block handler must be provided." unless executable
61
- raise "A client must be callable" unless executable.respond_to?(:call)
60
+ raise StitchingError, "A client or block handler must be provided." unless executable
61
+ raise StitchingError, "A client must be callable" unless executable.respond_to?(:call)
62
62
  end
63
63
  @executables[location] = executable
64
64
  end
65
65
 
66
- def execute_at_location(location, query, variables)
66
+ def execute_at_location(location, query, variables, context)
67
67
  executable = executables[location]
68
68
 
69
69
  if executable.nil?
70
- raise "No executable assigned for #{location} location."
70
+ raise StitchingError, "No executable assigned for #{location} location."
71
71
  elsif executable.is_a?(Class) && executable <= GraphQL::Schema
72
72
  executable.execute(
73
73
  query: query,
74
74
  variables: variables,
75
+ context: context,
75
76
  validate: false,
76
77
  )
77
78
  elsif executable.respond_to?(:call)
78
- executable.call(location, query, variables)
79
+ executable.call(location, query, variables, context)
79
80
  else
80
- raise "Missing valid executable for #{location} location."
81
+ raise StitchingError, "Missing valid executable for #{location} location."
81
82
  end
82
83
  end
83
84