Created
December 16, 2023 22:40
-
-
Save mikeharty/2fe77c8d014e588fecf8026b8db3bd76 to your computer and use it in GitHub Desktop.
Customized version of GraphQL::Stitching::HttpExecutable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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