graphql-client
graphql-client copied to clipboard
Any recommendations for configuring host dynamically for testing or development?
The layout and API of this gem encourage the use of constants to store loaded schemas, client objects, and parsed queries (there's even a special error to flag violations of these). I believe the motivation for this is to save developers from accidentally incurring the cost of parsing schemas or queries more often than is necessary, and to raise any validation errors from parsing an invalid schema as early as possible.
However there's one use-case that I've found becomes tricky with this setup, which is customizing the host on the fly. My instinct, based on how I've seen other Ruby API client libraries work in the past, would have been to take something like this:
HTTP = GraphQL::Client::HTTP.new("https://example.com/graphql") do
def headers(context)
# Optionally set any HTTP headers
{ "User-Agent": "My Client" }
end
end
and wrap it in a client object which could take the host as a constructor parameter. But obviously that doesn't work as well when everything is bootstrapped at require time.
Does anyone have tips on how to handle this problem? Do you just have to configure it through some other means that can be toggled at load-time (like a config file or an env variable or something)? Curious how others are handling test/dev/prod-type of host switching for these clients without duplicating too much of the code in order to provide multiple versions.
Just had to work around the same thing, and I agree the design is problematic in this case. I ended up subclassing the transport to hack around it a bit:
class ContextDrivenTransport < GraphQL::Client::HTTP
def initialize
super("https://localhost:1234")
end
def headers(context)
{
'Authorization' => "Bearer #{context.fetch(:api_key)}"
}
end
def execute(document:, operation_name: nil, variables: {}, context: {})
@uri = context.fetch(:base_uri) + '/graphql'
super(document: document, operation_name: operation_name, variables: variables, context: context)
end
end
This requires that my actual queries have context for the base uri as well as the api key to authenticate, which I then wrapped in a handy method on my Client wrapper class:
class MyClient
Schema = GraphQL::Client.load_schema(File.join(__dir__, 'graphql', 'schema.json'))
Client = GraphQL::Client.new(schema: Schema, execute: ContextDrivenTransport.new)
Queries = Client.parse('... some queries here ...')
def initialize(base_uri, api_key)
@base_uri = URI.parse(base_uri)
@api_key = api_key
end
def query(query, variables: {})
Client.query(query, variables: variables, context: { api_key: @api_key, base_uri: @base_uri })
end
end
(Slimmed down the example, my client implements some methods to make simple queries trivial as well, but you get the idea.)
Hope that helps!
and I agree the design is problematic in this case
Especially in cases like Shopify (which had their own problems) where each shop has their own endpoint but the underlying GQL schema is the same.
It's confusing and, really what's the upside to this small headache?
I ultimately opted for:
class GQL
#
# <snip> unrelated stuff
#
def initialize(endpoint, headers = nil)
raise ArgumentError, "endpoint required" if endpoint.to_s.strip.empty?
@endpoint = endpoint
@headers = headers || {}
end
def query
post(query)
end
private
def parse_json(data)
JSON.parse(data)
rescue JSON::ParserError => e
error("cannot parse JSON #{e}")
end
def error(message)
raise Error, "GraphQL query to #@endpoint failed: #{message}"
end
def post(query)
begin
response = Net::HTTP.post(@endpoint, query, @headers)
rescue => e
error(e.message)
end
error("#{response.body[0..100]}") if response.code != "200"
json = parse_json(response.body)
# In my cases there will never be "data" and "errors"
if json.include?("errors")
error(json["errors"].map { |e| e["message"] }.to_s)
end
json
end
end
Please tell me why this is bad?
and I agree the design is problematic in this case
Especially in cases like Shopify (which had their own problems) where each shop has their own endpoint but the underlying GQL schema is the same.
It's confusing and, really what's the upside to this small headache?
I ultimately opted for:
class GQL # # <snip> unrelated stuff # def initialize(endpoint, headers = nil) raise ArgumentError, "endpoint required" if endpoint.to_s.strip.empty? @endpoint = endpoint @headers = headers || {} end def query post(query) end private def parse_json(data) JSON.parse(data) rescue JSON::ParserError => e error("cannot parse JSON #{e}") end def error(message) raise Error, "GraphQL query to #@endpoint failed: #{message}" end def post(query) begin response = Net::HTTP.post(@endpoint, query, @headers) rescue => e error(e.message) end error("#{response.body[0..100]}") if response.code != "200" json = parse_json(response.body) # In my cases there will never be "data" and "errors" if json.include?("errors") error(json["errors"].map { |e| e["message"] }.to_s) end json end endPlease tell me why this is bad?
I think there is nothing wrong with it, you are simply skipping the grahql-client validations that check that queries comply with the GraphQL and schema specifications.