Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GraphQL::Query::Partial #5183

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion lib/graphql/execution/interpreter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl
case opts
when Hash
schema.query_class.new(schema, nil, **opts)
when GraphQL::Query
when GraphQL::Query, GraphQL::Query::Partial
opts
else
raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
Expand Down
9 changes: 7 additions & 2 deletions lib/graphql/execution/interpreter/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,13 @@ def inspect
# @return [void]
def run_eager
root_operation = query.selected_operation
root_op_type = root_operation.operation_type || "query"
root_type = schema.root_type_for_operation(root_op_type)
# TODO duck-type #root_type
root_type = if query.is_a?(GraphQL::Query::Partial)
query.root_type
else
root_op_type = root_operation.operation_type || "query"
schema.root_type_for_operation(root_op_type)
end
runtime_object = root_type.wrap(query.root_value, context)
runtime_object = schema.sync_lazy(runtime_object)
is_eager = root_op_type == "mutation"
Expand Down
13 changes: 13 additions & 0 deletions lib/graphql/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Query
autoload :Context, "graphql/query/context"
autoload :Fingerprint, "graphql/query/fingerprint"
autoload :NullContext, "graphql/query/null_context"
autoload :Partial, "graphql/query/partial"
autoload :Result, "graphql/query/result"
autoload :Variables, "graphql/query/variables"
autoload :InputValidationResult, "graphql/query/input_validation_result"
Expand Down Expand Up @@ -248,6 +249,18 @@ def operations
with_prepared_ast { @operations }
end

# Run subtree partials of this query and return their results.
# Each partial is identified with a `path => object` pair
# where the path references a field in the AST and the object will be treated
# as the return value from that field. Subfields of the field named by `path`
# will be executed with `object` as the starting point
# @param partials_hash [Hash<Array<String> => Object>] `path => object` pairs
# @return [Array<GraphQL::Query::Result>]
def run_partials(partials_hash)
partials = partials_hash.map { |path, obj| Partial.new(path: path, object: obj, query: self) }
Execution::Interpreter.run_all(@schema, partials, context: @context)
end

# Get the result for this query, executing it once
# @return [GraphQL::Query::Result] A Hash-like GraphQL response, with `"data"` and/or `"errors"` keys
def result
Expand Down
3 changes: 0 additions & 3 deletions lib/graphql/query/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ def initialize(query:, schema: query.schema, values:)
@storage = Hash.new { |h, k| h[k] = {} }
@storage[nil] = @provided_values
@errors = []
@path = []
@value = nil
@context = self # for SharedMethods TODO delete sharedmethods
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were leftover from a previous GraphQL::Query::Context implementation. I rediscovered them while working on this feature because at first, I thought I was going to need to use query.context.dup. I didn't end up doing that (instead, Query::Context.new inside Partial#initialize) but I'm going to keep these clean-ups.

@scoped_context = ScopedContext.new(self)
end

Expand Down
90 changes: 90 additions & 0 deletions lib/graphql/query/partial.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true
module GraphQL
class Query
# This class is _like_ a {GraphQL::Query}, except
# @see Query#run_partials
class Partial
def initialize(path:, object:, query:)
@path = path
@object = object
@query = query
@context = GraphQL::Query::Context.new(query: self, schema: @query.schema, values: @query.context.to_h)
@multiplex = nil
@result_values = nil
@result = nil
end

attr_reader :context

attr_accessor :multiplex, :result_values

def result
@result ||= GraphQL::Query::Result.new(query: self, values: result_values)
end

def valid?
true
end

def analyzers
EmptyObjects::EMPTY_ARRAY
end

def current_trace
@query.current_trace
end

def analysis_errors=(_errs)
end
rmosolgo marked this conversation as resolved.
Show resolved Hide resolved

def subscription?
false
end

def selected_operation
selection = @query.selected_operation
@path.each do |name_in_doc|
selection = selection.selections.find { |sel| sel.alias == name_in_doc || sel.name == name_in_doc }
end
selection
rmosolgo marked this conversation as resolved.
Show resolved Hide resolved
end

def schema
@query.schema
end

def types
@query.types
end

def root_value
@object
end

def root_type
# Eventually do the traversal upstream of here, processing the group of partials together.
selection = @query.selected_operation
type = @query.schema.query # TODO could be other?
@path.each do |name_in_doc|
selection = selection.selections.find { |sel| sel.alias == name_in_doc || sel.name == name_in_doc }
field_defn = type.get_field(selection.name, @query.context) || raise("Invariant: no field called #{selection.name.inspect} on #{type.graphql_name}")
type = field_defn.type.unwrap
end
type
end

# TODO dry with query
def after_lazy(value, &block)
if !defined?(@runtime_instance)
@runtime_instance = context.namespace(:interpreter_runtime)[:runtime]
end

if @runtime_instance
@runtime_instance.minimal_after_lazy(value, &block)
else
@schema.after_lazy(value, &block)
end
end
end
end
end
71 changes: 71 additions & 0 deletions spec/graphql/query/partial_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true
require "spec_helper"

describe GraphQL::Query::Partial do
class PartialSchema < GraphQL::Schema
FARMS = {
"1" => OpenStruct.new(name: "Bellair Farm", products: ["VEGETABLES", "MEAT", "EGGS"]),
"2" => OpenStruct.new(name: "Henley's Orchard", products: ["FRUIT", "MEAT", "EGGS"]),
"3" => OpenStruct.new(name: "Wenger Grapes", products: ["FRUIT"]),
}

class FarmProduct < GraphQL::Schema::Enum
value :FRUIT
value :VEGETABLES
value :MEAT
value :EGGS
value :DAIRY
end

class Farm < GraphQL::Schema::Object
field :name, String
field :products, [FarmProduct]
end

class Query < GraphQL::Schema::Object
field :farms, [Farm], fallback_value: FARMS.values

field :farm, Farm do
argument :id, ID, loads: Farm, as: :farm
end

def farm(farm:)
farm
end
end

query(Query)

def self.object_from_id(id, ctx)
FARMS[id]
end

def self.resolve_type(abs_type, object, ctx)
Farm
end
end

focus
it "returns results for the named part" do
str = "{
farms { name, products }
farm1: farm(id: \"1\") { name }
farm2: farm(id: \"2\") { name }
}"
query = GraphQL::Query.new(PartialSchema, str)
results = query.run_partials(
["farm1"] => PartialSchema::FARMS["1"],
["farm2"] => OpenStruct.new(name: "Injected Farm"),
)

assert_equal [
{ "data" => { "name" => "Bellair Farm" } },
{ "data" => { "name" => "Injected Farm" } },
], results
end

it "returns errors if they occur"
it "raises errors when given bad paths"
it "runs multiple partials concurrently"
it "returns multiple errors concurrently"
end
Loading