lib/graphql/stitching/http_executable.rb in graphql-stitching-1.1.1 vs lib/graphql/stitching/http_executable.rb in graphql-stitching-1.2.0
- old
+ new
@@ -5,21 +5,151 @@
require "json"
module GraphQL
module Stitching
class HttpExecutable
- def initialize(url:, headers:{})
+ def initialize(url:, headers:{}, upload_types: nil)
@url = url
@headers = { "Content-Type" => "application/json" }.merge!(headers)
+ @upload_types = upload_types
end
- def call(_location, document, variables, _context)
- response = Net::HTTP.post(
+ def call(request, document, variables)
+ multipart_form = if request.variable_definitions.any? && variables&.any?
+ extract_multipart_form(request, document, variables)
+ end
+
+ response = if multipart_form
+ post_multipart(multipart_form)
+ else
+ post(document, variables)
+ end
+
+ JSON.parse(response.body)
+ end
+
+ def post(document, variables)
+ Net::HTTP.post(
URI(@url),
JSON.generate({ "query" => document, "variables" => variables }),
@headers,
)
- JSON.parse(response.body)
+ end
+
+ def post_multipart(form_data)
+ uri = URI(@url)
+ req = Net::HTTP::Post.new(uri)
+ @headers.each_pair do |key, value|
+ req[key] = value
+ end
+
+ req.set_form(form_data.to_a, "multipart/form-data")
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
+ http.request(req)
+ end
+ end
+
+ # extract multipart upload forms
+ # spec: https://github.com/jaydenseric/graphql-multipart-request-spec
+ def extract_multipart_form(request, document, variables)
+ return unless @upload_types
+
+ path = []
+ files_by_path = {}
+
+ # extract all upload scalar values mapped by their input path
+ variables.each do |key, value|
+ ast_node = request.variable_definitions[key]
+ path << key
+ extract_ast_node(ast_node, value, files_by_path, path, request) if ast_node
+ path.pop
+ end
+
+ return if files_by_path.none?
+
+ map = {}
+ files = files_by_path.values.tap(&:uniq!)
+ variables_copy = variables.dup
+
+ files_by_path.keys.each do |path|
+ orig = variables
+ copy = variables_copy
+ path.each_with_index do |key, i|
+ if i == path.length - 1
+ map_key = files.index(copy[key]).to_s
+ map[map_key] ||= []
+ map[map_key] << "variables.#{path.join(".")}"
+ copy[key] = nil
+ elsif orig[key].object_id == copy[key].object_id
+ copy[key] = copy[key].dup
+ end
+ orig = orig[key]
+ copy = copy[key]
+ end
+ end
+
+ form = {
+ "operations" => JSON.generate({
+ "query" => document,
+ "variables" => variables_copy,
+ }),
+ "map" => JSON.generate(map),
+ }
+
+ files.each_with_object(form).with_index do |(file, memo), index|
+ memo[index.to_s] = file.respond_to?(:tempfile) ? file.tempfile : file
+ end
+ end
+
+ private
+
+ def extract_ast_node(ast_node, value, files_by_path, path, request)
+ return unless value
+
+ ast_node = ast_node.of_type while ast_node.is_a?(GraphQL::Language::Nodes::NonNullType)
+
+ if ast_node.is_a?(GraphQL::Language::Nodes::ListType)
+ if value.is_a?(Array)
+ value.each_with_index do |val, index|
+ path << index
+ extract_ast_node(ast_node.of_type, val, files_by_path, path, request)
+ path.pop
+ end
+ end
+ elsif @upload_types.include?(ast_node.name)
+ files_by_path[path.dup] = value
+ else
+ type_def = request.supergraph.schema.get_type(ast_node.name)
+ extract_type_node(type_def, value, files_by_path, path) if type_def&.kind&.input_object?
+ end
+ end
+
+ def extract_type_node(parent_type, value, files_by_path, path)
+ return unless value
+
+ parent_type = Util.unwrap_non_null(parent_type)
+
+ if parent_type.list?
+ if value.is_a?(Array)
+ value.each_with_index do |val, index|
+ path << index
+ extract_type_node(parent_type.of_type, val, files_by_path, path)
+ path.pop
+ end
+ end
+ elsif parent_type.kind.input_object?
+ if value.is_a?(Enumerable)
+ arguments = parent_type.arguments
+ value.each do |key, val|
+ arg_type = arguments[key]&.type
+ path << key
+ extract_type_node(arg_type, val, files_by_path, path) if arg_type
+ path.pop
+ end
+ end
+ elsif @upload_types.include?(parent_type.graphql_name)
+ files_by_path[path.dup] = value
+ end
end
end
end
end