Last active
November 20, 2023 17:20
-
-
Save rmosolgo/a51486fdf5691e6ddcc5b44736fdabad to your computer and use it in GitHub Desktop.
Using GraphQL-Ruby 1.12 with @defer and { backtrace: true }
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
require "bundler/inline" | |
gemfile do | |
gem "graphql", "1.12.18" | |
gem "graphql-pro", "1.24.13" | |
gem "graphql-batch" | |
end | |
# Monkey-patch this to avoid deleting keys from multiplex.context | |
# in the ensure block below. | |
# (Fixed on new GraphQL-Ruby versions in https://github.com/rmosolgo/graphql-ruby/pull/4708) | |
module GraphQL | |
class Backtrace | |
module Tracer | |
module_function | |
def trace(key, metadata) | |
case key | |
when "lex", "parse" | |
# No context here, don't have a query yet | |
nil | |
when "execute_multiplex", "analyze_multiplex" | |
# No query context yet | |
nil | |
when "validate", "analyze_query", "execute_query", "execute_query_lazy" | |
push_key = [] | |
if (query = metadata[:query]) || ((queries = metadata[:queries]) && (query = queries.first)) | |
push_data = query | |
multiplex = query.multiplex | |
elsif (multiplex = metadata[:multiplex]) | |
push_data = multiplex.queries.first | |
end | |
when "execute_field", "execute_field_lazy" | |
query = metadata[:query] || raise(ArgumentError, "Add `legacy: true` to use GraphQL::Backtrace without the interpreter runtime.") | |
multiplex = query.multiplex | |
push_key = metadata[:path] | |
parent_frame = multiplex.context[:graphql_backtrace_contexts][push_key[0..-2]] | |
if parent_frame.is_a?(GraphQL::Query) | |
parent_frame = parent_frame.context | |
end | |
push_data = Frame.new( | |
query: query, | |
path: push_key, | |
ast_node: metadata[:ast_node], | |
field: metadata[:field], | |
object: metadata[:object], | |
arguments: metadata[:arguments], | |
parent_frame: parent_frame, | |
) | |
else | |
# Custom key, no backtrace data for this | |
nil | |
end | |
if push_data && multiplex | |
push_storage = multiplex.context[:graphql_backtrace_contexts] ||= {} | |
push_storage[push_key] = push_data | |
multiplex.context[:last_graphql_backtrace_context] = push_data | |
end | |
if key == "execute_multiplex" | |
multiplex_context = metadata[:multiplex].context | |
begin | |
yield | |
rescue StandardError => err | |
# This is an unhandled error from execution, | |
# Re-raise it with a GraphQL trace. | |
potential_context = multiplex_context[:last_graphql_backtrace_context] | |
if potential_context.is_a?(GraphQL::Query::Context) || | |
potential_context.is_a?(GraphQL::Query::Context::FieldResolutionContext) || | |
potential_context.is_a?(Backtrace::Frame) | |
raise TracedError.new(err, potential_context) | |
else | |
raise | |
end | |
# Don't clean these up -- leave them for @defer to use | |
# ensure | |
# multiplex_context.delete(:graphql_backtrace_contexts) | |
# multiplex_context.delete(:last_graphql_backtrace_context) | |
end | |
else | |
yield | |
end | |
end | |
end | |
end | |
end | |
# Use the example from https://graphql-ruby.org/defer/setup.html#with-graphql-batch | |
module Directives | |
# Modify the library's `@defer` implementation to work with GraphQL-Batch | |
class Defer < GraphQL::Pro::Defer | |
def self.resolve(obj, arguments, context, &block) | |
# While the query is running, store the batch executor to re-use later | |
context[:graphql_batch_executor] ||= GraphQL::Batch::Executor.current | |
super | |
end | |
class Deferral < GraphQL::Pro::Defer::Deferral | |
def resolve | |
# Before calling the deferred execution, | |
# set GraphQL-Batch back up: | |
prev_executor = GraphQL::Batch::Executor.current | |
GraphQL::Batch::Executor.current ||= @context[:graphql_batch_executor] | |
super | |
ensure | |
# Clean up afterward: | |
GraphQL::Batch::Executor.current = prev_executor | |
end | |
end | |
end | |
end | |
require "ostruct" | |
class Schema < GraphQL::Schema | |
class UserLoader < GraphQL::Batch::Loader | |
def perform(ids) | |
p "Fetching IDs: #{ids}" | |
ids.each { |id| fulfill(id, OpenStruct.new({ name: "User ##{id}" })) } | |
end | |
end | |
class User < GraphQL::Schema::Object | |
field :name, String, null: false | |
def name | |
user.then(&:name) | |
end | |
private | |
def user | |
UserLoader.load(object) | |
end | |
end | |
class UserCollection < GraphQL::Schema::Object | |
field :collection, [User], null: false | |
def collection | |
object # [1, 2, 3] | |
end | |
end | |
class Query < GraphQL::Schema::Object | |
field :users, UserCollection, null: false | |
def users | |
[1, 2, 3] | |
end | |
end | |
query(Query) | |
use Directives::Defer | |
use GraphQL::Batch | |
end | |
query_str = "{ users { collection { name @defer } } }" | |
res = Schema.execute(query_str, context: { backtrace: false }) | |
pp res.to_h | |
res.context[:defer].each do |deferral| | |
pp [:deferred, deferral.to_h] | |
end | |
# {"data"=>{"users"=>{"collection"=>[{}, {}, {}]}}} | |
# [:deferred, {:hasNext=>true, :data=>{"users"=>{"collection"=>[{}, {}, {}]}}}] | |
# "Fetching IDs: [1]" | |
# [:deferred, {:path=>["users", "collection", 0, "name"], :hasNext=>true, :data=>"User #1"}] | |
# "Fetching IDs: [2]" | |
# [:deferred, {:path=>["users", "collection", 1, "name"], :hasNext=>true, :data=>"User #2"}] | |
# "Fetching IDs: [3]" | |
# [:deferred, {:path=>["users", "collection", 2, "name"], :hasNext=>false, :data=>"User #3"}] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment