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