graphql-stitching 0.0.1 → 0.2.1

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.
@@ -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