Skip to content

Instantly share code, notes, and snippets.

@mikeharty
Created December 16, 2023 22:40
Show Gist options
  • Save mikeharty/2fe77c8d014e588fecf8026b8db3bd76 to your computer and use it in GitHub Desktop.
Save mikeharty/2fe77c8d014e588fecf8026b8db3bd76 to your computer and use it in GitHub Desktop.
Customized version of GraphQL::Stitching::HttpExecutable
module Supergraph
# Custom HttpExecutable
# - Uses RestClient instead of Net::HTTP
# - Adds Authenticator
# - Adds Guard
# - Adds pre and post processing hooks
# - Handles multipart form file uploads
class HttpExecutable < GraphQL::Stitching::HttpExecutable
attr_accessor :uri, :headers
# @param [URI] uri The URI to send GraphQL requests to
# @param [Authenticator] authenticator An Authenticator to facilitate authentication
# @param [Guard] guard A guard to perform query authorization
# @param [Proc] preprocessor A proc able to mutate the doc and vars in place before sending
# @param [Proc] postprocessor A proc able to mutate the response in place before sending
# @param [Hash] headers A hash of headers to send with the request
# @return [Hash] The response from the request
# @example
# Supergraph::HttpExecutable.new(
# URI('https://api.example.com/graphql'),
# authenticator: Supergraph::Authenticators::Cognito.new(...)
# guard: Supergraph::Guards::Example.new(...)
# preprocessor: ->(document, variables) { variables['test'] = 2 },
# postprocessor: ->(response) { response['foo'] = 'bar' }
# )
def initialize(
uri,
guard: nil,
headers: {},
preprocessors: [],
postprocessors: [],
authenticator: nil
)
@uri = uri
@guard = guard
@headers = headers
@preprocessors = preprocessors
@postprocessors = postprocessors
@authenticator = authenticator
end
def call(_location, document, variables, context)
# Subgraph requests don't pass through Rails controllers, so the parameters
# are not known to be safe. Assume the external graph will handle sanitization.
variables = variables.to_unsafe_h if variables.is_a?(ActionController::Parameters)
# Parse the document
document = GraphQL.parse(document)
# Run the preprocessor hooks
@preprocessors.each { |p| p.call(document, response, context) }
# Extract the operation name
operation = document.definitions.find { |d| d.is_a?(GraphQL::Language::Nodes::OperationDefinition) }&.name
# Perform local authorization
@guard&.authorize!(document, variables, context)
# If the document is not already a query string, stringify it
document = document.to_query_string unless document.is_a?(String)
# Perform remote authentication
@authenticator&.perform(self)
# Build request options
options = {
operations: {
query: document,
operationName: operation,
variables: variables
}
}
# Create a mutable copy of headers
req_headers = headers.dup
req_headers['Content-Type'] = req_headers&.fetch('Content-Type', 'application/json')
# Map files to variables, if present
multipart_form_files(options, variables, req_headers) unless variables.fetch('files', nil).nil?
# Stringify the operations
options[:operations] = options[:operations].to_json
# Reduce to a single JSON value if we're not sending files
options = options[:operations] if options[:map].nil?
# Send it off
RestClient.post(@uri.to_s, options, req_headers) do |response, _request, _result|
response = JSON.parse(response.body)
# Run the postprocessor hooks
@postprocessors.each { |p| p.call(response, context) }
# Return the response
response
end
end
private
# The Apollo Upload Server maps uploaded files to variables,
# this maps them back onto the request options and nulls the
# variables so the next server can do the same.
def multipart_form_files(options, variables, headers)
options[:map] = {}
files = variables['files']
files = [files] unless files.is_a?(Array)
return unless files.any?
files.each_with_index do |file, index|
options[index.to_s] = file.tempfile
options[:map][index.to_s] = ["variables.files.#{index}"]
end
variables['files'] = files.dup.fill(nil)
options[:map] = options[:map].to_json
headers['Content-Type'] = 'multipart/form-data'
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment