Skip to content

Instantly share code, notes, and snippets.

@redjoker011
Created November 3, 2020 12:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save redjoker011/bba957efe2cbfa3bd1b4702183c5b4e9 to your computer and use it in GitHub Desktop.
Save redjoker011/bba957efe2cbfa3bd1b4702183c5b4e9 to your computer and use it in GitHub Desktop.
Ruby GraphQL File Uploader
require "graphql/client"
require "graphql/client/http"
class GraphqlClient::Base
# Configure GraphQL endpoint using the basic HTTP network adapter.
endpoint = ENV.fetch("GRAPHQL_ENDPOINT")
HTTP = GraphqlClient::CustomHTTP.new(endpoint)
# Fetch latest schema on init, this will make a network request
# Schema = GraphQL::Client.load_schema(HTTP)
# However, it's smart to dump this to a JSON file and load from disk
schema_path = "lib/graphql/schema.json"
Schema = GraphQL::Client.load_schema(schema_path)
# Client = GraphQL::Client.new(schema: Schema, execute: HTTP)
Client = GraphqlClient::CustomClient.new(schema: Schema, execute: HTTP)
end
require "graphql/client"
# Graphql Custom Client
# Use Duck Typing to extend library instance methods and bind logger
# @author Peter John Alvarado <peterjohn@gorated.ph>
class GraphqlClient::CustomClient < GraphQL::Client
def initialize(schema:, execute: nil, enforce_collocated_callers: false)
super(
schema: schema,
execute: execute,
enforce_collocated_callers: enforce_collocated_callers
)
end
def query(definition, variables: {})
body = {
"query": definition.document.to_query_string,
"variables": variables.inspect,
"operationName": definition.operation_name
}
Rails.logger.debug("[GraphqlClient] Query String: #{body.to_json}\n")
super(definition, variables: variables)
end
end
require "graphql/client/http"
# Graphql Custom Client
# Use Duck Typing to extend library instance methods and support File Upload
# This approach is similar inspired by an existing pull request in
# graphql-client gem to handle file upload unfortunately this pull request
# hasn't yet merged due to conflict and some bugs which we address here
# @see https://github.com/github/graphql-client/pull/236/commits/19699c3a128edc425a6594b842e8958c709a5710
#
# @author Peter John Alvarado <peterjohn@gorated.ph>
#
# rubocop:disable all
class GraphqlClient::CustomHTTP < GraphQL::Client::HTTP
# Public: Make an HTTP request for GraphQL query.
#
# Implements Client's "execute" adapter interface.
#
# document - The Query GraphQL::Language::Nodes::Document
# operation_name - The String operation definition name
# variables - Hash of query variables
# context - An arbitrary Hash of values which you can access
#
# Returns { "data" => ... , "errors" => ... } Hash.
def execute(document:, operation_name: nil, variables: {}, context: {})
# Setup default request details
# @see https://github.com/github/graphql-client/blob/master/lib/graphql/client/http.rb#L58
req = Net::HTTP::Post.new(uri.request_uri)
req.basic_auth(uri.user, uri.password) if uri.user || uri.password
req["Accept"] = "application/json"
headers(context).each { |name, value| request[name] = value }
form_fields = form_data!(variables)
body = {}
body["query"] = document.to_query_string
body["variables"] = variables if variables.any?
body["operationName"] = operation_name if operation_name
# Set request to multipart if file upload is present on the payload
if form_fields
# post as multipart/form-data to stream file contents
payload = { operations: JSON.generate(body) }.merge(form_fields)
req.set_form(payload.stringify_keys, "multipart/form-data")
else
# post as application/json
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
response = connection.request(req)
case response
when Net::HTTPOK, Net::HTTPBadRequest
JSON.parse(response.body)
else
{ "errors" => [{ "message" => "#{response.code} #{response.message}" }] }
end
end
private
# generate the form data for a multipart request according to the GraphQL multipart request
# @see specification (https://github.com/jaydenseric/graphql-multipart-request-spec/)
#
# Example request form data:
# operations: {"query": "…", "operationName": "addToGallery", "variables": {"galleryId": "…", images: [null, null, null]}}
# map: {"1": ["variables.images.0"], "2": ["variables.images.1"], "3": ["variables.images.2"]}
# 1: File
# 2: File
# 3: File
#
# note: modifies `variables`, returns form data (except `operations`) or `nil`
def form_data!(variables)
form = {}
file_map = {}
# recursively walk `variables` looking for `File` values, add them to the form data,
# then replace with `nil`
stack = variables.map { |k, v| [ variables, ['variables', k], v ] }
while (variable, path, val = stack.pop) do
if val.is_a?(Hash)
val.each { |k, v| stack.push [ val, path.dup << k, v ] }
elsif val.is_a?(Array)
val.each.with_index { |v, i| stack.push [ val, (path.dup << i), v ] }
# elsif val.is_a?(IO)
elsif val.is_a?(ActionDispatch::Http::UploadedFile)
idx = file_map.length + 1
file_map[idx.to_s] = [ path.map(&:to_s).join('.') ]
# NOTE: Rails converted file upload object to ActionDispatch::Http::UploadedFile
# since we need the File object and not
# ActionDispatch::Http::UploadedFile object we will use `#open`
# else we will received binary in our api server
form[idx.to_s] = val.open
variable[path.last] = nil # replace `File` value with `nil` in `variables`
end
end
form.presence && { map: JSON.generate(file_map) }.merge(form)
end
end
# rubocop:enable all
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment