diff --git a/guides/_layouts/default.html b/guides/_layouts/default.html
index 8053cb4d82..2404663b23 100644
--- a/guides/_layouts/default.html
+++ b/guides/_layouts/default.html
@@ -44,6 +44,7 @@
Guides
Configuration Options
Code Reuse
Instrumentation
+ Lazy Execution
Limiting Visibility
Testing
diff --git a/guides/schema/lazy_execution.md b/guides/schema/lazy_execution.md
new file mode 100644
index 0000000000..d9af8e01e2
--- /dev/null
+++ b/guides/schema/lazy_execution.md
@@ -0,0 +1,84 @@
+---
+title: Schema — Lazy Execution
+---
+
+With lazy execution, you can optimize access to external services (such as databases) by making batched calls. Building a lazy loader has three steps:
+
+- Define a lazy-loading class with _one_ method for loading & returning a value
+- Connect it to your schema with {{ "GraphQL::Schema#lazy_resolve" | api_doc }}
+- In `resolve` functions, return instances of the lazy-loading class
+
+[`graphql-batch`](https://github.com/shopify/graphql-batch) provides a powerful, flexible toolkit for lazy resolution with GraphQL.
+
+## Example: Batched Find
+
+Here's a way to find many objects by ID using one database call, preventing N+1 queries.
+
+1. Lazy-loading class which finds models by ID.
+
+ ```ruby
+ class LazyFindPerson
+ def initialize(query_ctx, person_id)
+ @person_id = person_id
+ # Initialize the loading state for this query,
+ # or get the previously-initiated state
+ @lazy_state = query_ctx[:lazy_find_person] ||= {
+ pending_ids: [],
+ loaded_ids: {},
+ }
+ # Register this ID to be loaded later:
+ @lazy_state[:pending_ids] << person_id
+ end
+
+ # Return the loaded record, hitting the database if needed
+ def person
+ # Check if the record was already loaded:
+ loaded_record = @lazy_state[:loaded_ids][@person_id]
+ if loaded_record
+ # The pending IDs were already loaded,
+ # so return the result of that previous load
+ loaded_record
+ else
+ # The record hasn't been loaded yet, so
+ # hit the database with all pending IDs
+ pending_ids = @lazy_state[:pending_ids]
+ people = Person.where(id: pending_ids)
+ people.each { |person| @lazy_state[:loaded_ids][person.id] = person }
+ # Now, get the matching person from the loaded result:
+ @lazy_state[:loaded_ids][@person_id]
+ end
+ end
+ ```
+
+2. Connect the lazy resolve method
+
+ ```ruby
+ MySchema = GraphQL::Schema.define do
+ # ...
+ lazy_resolve(LazyFindPerson, :person)
+ end
+ ```
+
+3. Return lazy objects from `resolve`
+
+ ```ruby
+ field :author, PersonType do
+ resolve ->(obj, args, ctx) {
+ LazyFindPerson(ctx, obj.author_id)
+ }
+ end
+ ```
+
+Now, calls to `author` will use batched database access. For example, this query:
+
+```graphql
+{
+ p1: post(id: 1) { author { name } }
+ p2: post(id: 2) { author { name } }
+ p3: post(id: 3) { author { name } }
+}
+```
+
+Will only make one query to load the `author` values.
+
+The example above is simple and has some shortcomings. Consider the `graphql-batch` gem for a robust solution to batched resolution.
diff --git a/lib/graphql.rb b/lib/graphql.rb
index ed42b2c6ac..7143f15016 100644
--- a/lib/graphql.rb
+++ b/lib/graphql.rb
@@ -67,6 +67,7 @@ def self.scan_with_ragel(query_string)
require "graphql/introspection"
require "graphql/language"
require "graphql/analysis"
+require "graphql/execution"
require "graphql/schema"
require "graphql/schema/loader"
require "graphql/schema/printer"
@@ -81,5 +82,4 @@ def self.scan_with_ragel(query_string)
require "graphql/static_validation"
require "graphql/version"
require "graphql/relay"
-require "graphql/execution"
require "graphql/compatibility"
diff --git a/lib/graphql/compatibility.rb b/lib/graphql/compatibility.rb
index 4068dc71a7..443d68f4af 100644
--- a/lib/graphql/compatibility.rb
+++ b/lib/graphql/compatibility.rb
@@ -1,3 +1,4 @@
require "graphql/compatibility/execution_specification"
+require "graphql/compatibility/lazy_execution_specification"
require "graphql/compatibility/query_parser_specification"
require "graphql/compatibility/schema_parser_specification"
diff --git a/lib/graphql/compatibility/execution_specification.rb b/lib/graphql/compatibility/execution_specification.rb
index c8091551ef..61dd8fc321 100644
--- a/lib/graphql/compatibility/execution_specification.rb
+++ b/lib/graphql/compatibility/execution_specification.rb
@@ -1,3 +1,6 @@
+require "graphql/compatibility/execution_specification/counter_schema"
+require "graphql/compatibility/execution_specification/specification_schema"
+
module GraphQL
module Compatibility
# Test an execution strategy. This spec is not meant as a development aid.
@@ -22,164 +25,22 @@ module Compatibility
# - Relay features
#
module ExecutionSpecification
- DATA = {
- "1001" => OpenStruct.new({
- name: "Fannie Lou Hamer",
- birthdate: Time.new(1917, 10, 6),
- organization_ids: [],
- }),
- "1002" => OpenStruct.new({
- name: "John Lewis",
- birthdate: Time.new(1940, 2, 21),
- organization_ids: ["2001"],
- }),
- "1003" => OpenStruct.new({
- name: "Diane Nash",
- birthdate: Time.new(1938, 5, 15),
- organization_ids: ["2001", "2002"],
- }),
- "1004" => OpenStruct.new({
- name: "Ralph Abernathy",
- birthdate: Time.new(1926, 3, 11),
- organization_ids: ["2002"],
- }),
- "2001" => OpenStruct.new({
- name: "SNCC",
- leader_id: nil, # fail on purpose
- }),
- "2002" => OpenStruct.new({
- name: "SCLC",
- leader_id: "1004",
- }),
- }
-
# Make a minitest suite for this execution strategy, making sure it
# fulfills all the requirements of this library.
# @param execution_strategy [<#new, #execute>] An execution strategy class
# @return [Class] A test suite for this execution strategy
def self.build_suite(execution_strategy)
Class.new(Minitest::Test) do
- def self.build_schema(execution_strategy)
- organization_type = nil
-
- timestamp_type = GraphQL::ScalarType.define do
- name "Timestamp"
- coerce_input ->(value) { Time.at(value.to_i) }
- coerce_result ->(value) { value.to_i }
- end
-
- named_entity_interface_type = GraphQL::InterfaceType.define do
- name "NamedEntity"
- field :name, !types.String
- end
-
- person_type = GraphQL::ObjectType.define do
- name "Person"
- interfaces [named_entity_interface_type]
- field :name, !types.String
- field :birthdate, timestamp_type
- field :age, types.Int do
- argument :on, !timestamp_type
- resolve ->(obj, args, ctx) {
- if obj.birthdate.nil?
- nil
- else
- age_on = args[:on]
- age_years = age_on.year - obj.birthdate.year
- this_year_birthday = Time.new(age_on.year, obj.birthdate.month, obj.birthdate.day)
- if this_year_birthday > age_on
- age_years -= 1
- end
- end
- age_years
- }
- end
- field :organizations, types[organization_type] do
- resolve ->(obj, args, ctx) {
- obj.organization_ids.map { |id| DATA[id] }
- }
- end
- field :first_organization, !organization_type do
- resolve ->(obj, args, ctx) {
- DATA[obj.organization_ids.first]
- }
- end
- end
-
- organization_type = GraphQL::ObjectType.define do
- name "Organization"
- interfaces [named_entity_interface_type]
- field :name, !types.String
- field :leader, !person_type do
- resolve ->(obj, args, ctx) {
- DATA[obj.leader_id] || (ctx[:return_error] ? ExecutionError.new("Error on Nullable") : nil)
- }
- end
- field :returnedError, types.String do
- resolve ->(o, a, c) {
- GraphQL::ExecutionError.new("This error was returned")
- }
- end
- field :raisedError, types.String do
- resolve ->(o, a, c) {
- raise GraphQL::ExecutionError.new("This error was raised")
- }
- end
-
- field :nodePresence, !types[!types.Boolean] do
- resolve ->(o, a, ctx) {
- [
- ctx.irep_node.is_a?(GraphQL::InternalRepresentation::Node),
- ctx.ast_node.is_a?(GraphQL::Language::Nodes::AbstractNode),
- false, # just testing
- ]
- }
- end
- end
-
- node_union_type = GraphQL::UnionType.define do
- name "Node"
- possible_types [person_type, organization_type]
- end
-
- query_type = GraphQL::ObjectType.define do
- name "Query"
- field :node, node_union_type do
- argument :id, !types.ID
- resolve ->(obj, args, ctx) {
- obj[args[:id]]
- }
- end
-
- field :organization, !organization_type do
- argument :id, !types.ID
- resolve ->(obj, args, ctx) {
- args[:id].start_with?("2") && obj[args[:id]]
- }
- end
-
- field :organizations, types[organization_type] do
- resolve ->(obj, args, ctx) {
- [obj["2001"], obj["2002"]]
- }
- end
- end
-
- GraphQL::Schema.define do
- query_execution_strategy execution_strategy
- query query_type
-
- resolve_type ->(obj, ctx) {
- obj.respond_to?(:birthdate) ? person_type : organization_type
- }
- end
+ class << self
+ attr_accessor :counter_schema, :specification_schema
end
- @@schema = build_schema(execution_strategy)
+ self.specification_schema = SpecificationSchema.build(execution_strategy)
+ self.counter_schema = CounterSchema.build(execution_strategy)
def execute_query(query_string, **kwargs)
- kwargs[:root_value] = DATA
- @@schema.execute(query_string, **kwargs)
+ kwargs[:root_value] = SpecificationSchema::DATA
+ self.class.specification_schema.execute(query_string, **kwargs)
end
def test_it_fetches_data
@@ -409,47 +270,7 @@ def test_it_doesnt_add_errors_for_invalid_nulls_from_execution_errors
end
def test_it_only_resolves_fields_once_on_typed_fragments
- count = 0
- counter_type = nil
-
- has_count_interface = GraphQL::InterfaceType.define do
- name "HasCount"
- field :count, types.Int
- field :counter, ->{ counter_type }
- end
-
- counter_type = GraphQL::ObjectType.define do
- name "Counter"
- interfaces [has_count_interface]
- field :count, types.Int, resolve: ->(o,a,c) { count += 1 }
- field :counter, has_count_interface, resolve: ->(o,a,c) { :counter }
- end
-
- alt_counter_type = GraphQL::ObjectType.define do
- name "AltCounter"
- interfaces [has_count_interface]
- field :count, types.Int, resolve: ->(o,a,c) { count += 1 }
- field :counter, has_count_interface, resolve: ->(o,a,c) { :counter }
- end
-
- has_counter_interface = GraphQL::InterfaceType.define do
- name "HasCounter"
- field :counter, counter_type
- end
-
- query_type = GraphQL::ObjectType.define do
- name "Query"
- interfaces [has_counter_interface]
- field :counter, has_count_interface, resolve: ->(o,a,c) { :counter }
- end
-
- schema = GraphQL::Schema.define(
- query: query_type,
- resolve_type: ->(o, c) { o == :counter ? counter_type : nil },
- orphan_types: [alt_counter_type],
- )
-
- res = schema.execute("
+ res = self.class.counter_schema.execute("
{
counter { count }
... on HasCounter {
@@ -462,10 +283,10 @@ def test_it_only_resolves_fields_once_on_typed_fragments
"counter" => { "count" => 1 }
}
assert_equal expected_data, res["data"]
- assert_equal 1, count
+ assert_equal 1, self.class.counter_schema.metadata[:count]
# Deep typed children are correctly distinguished:
- res = schema.execute("
+ res = self.class.counter_schema.execute("
{
counter {
... on Counter {
diff --git a/lib/graphql/compatibility/execution_specification/counter_schema.rb b/lib/graphql/compatibility/execution_specification/counter_schema.rb
new file mode 100644
index 0000000000..94f87fc79c
--- /dev/null
+++ b/lib/graphql/compatibility/execution_specification/counter_schema.rb
@@ -0,0 +1,52 @@
+module GraphQL
+ module Compatibility
+ module ExecutionSpecification
+ module CounterSchema
+ def self.build(execution_strategy)
+ counter_type = nil
+ schema = nil
+
+ has_count_interface = GraphQL::InterfaceType.define do
+ name "HasCount"
+ field :count, types.Int
+ field :counter, ->{ counter_type }
+ end
+
+ counter_type = GraphQL::ObjectType.define do
+ name "Counter"
+ interfaces [has_count_interface]
+ field :count, types.Int, resolve: ->(o,a,c) { schema.metadata[:count] += 1 }
+ field :counter, has_count_interface, resolve: ->(o,a,c) { :counter }
+ end
+
+ alt_counter_type = GraphQL::ObjectType.define do
+ name "AltCounter"
+ interfaces [has_count_interface]
+ field :count, types.Int, resolve: ->(o,a,c) { schema.metadata[:count] += 1 }
+ field :counter, has_count_interface, resolve: ->(o,a,c) { :counter }
+ end
+
+ has_counter_interface = GraphQL::InterfaceType.define do
+ name "HasCounter"
+ field :counter, counter_type
+ end
+
+ query_type = GraphQL::ObjectType.define do
+ name "Query"
+ interfaces [has_counter_interface]
+ field :counter, has_count_interface, resolve: ->(o,a,c) { :counter }
+ end
+
+ schema = GraphQL::Schema.define(
+ query: query_type,
+ resolve_type: ->(o, c) { o == :counter ? counter_type : nil },
+ orphan_types: [alt_counter_type],
+ query_execution_strategy: execution_strategy,
+ )
+ schema.metadata[:count] = 0
+ schema
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/compatibility/execution_specification/specification_schema.rb b/lib/graphql/compatibility/execution_specification/specification_schema.rb
new file mode 100644
index 0000000000..aa01aa408d
--- /dev/null
+++ b/lib/graphql/compatibility/execution_specification/specification_schema.rb
@@ -0,0 +1,154 @@
+module GraphQL
+ module Compatibility
+ module ExecutionSpecification
+ module SpecificationSchema
+ DATA = {
+ "1001" => OpenStruct.new({
+ name: "Fannie Lou Hamer",
+ birthdate: Time.new(1917, 10, 6),
+ organization_ids: [],
+ }),
+ "1002" => OpenStruct.new({
+ name: "John Lewis",
+ birthdate: Time.new(1940, 2, 21),
+ organization_ids: ["2001"],
+ }),
+ "1003" => OpenStruct.new({
+ name: "Diane Nash",
+ birthdate: Time.new(1938, 5, 15),
+ organization_ids: ["2001", "2002"],
+ }),
+ "1004" => OpenStruct.new({
+ name: "Ralph Abernathy",
+ birthdate: Time.new(1926, 3, 11),
+ organization_ids: ["2002"],
+ }),
+ "2001" => OpenStruct.new({
+ name: "SNCC",
+ leader_id: nil, # fail on purpose
+ }),
+ "2002" => OpenStruct.new({
+ name: "SCLC",
+ leader_id: "1004",
+ }),
+ }
+
+ def self.build(execution_strategy)
+ organization_type = nil
+
+ timestamp_type = GraphQL::ScalarType.define do
+ name "Timestamp"
+ coerce_input ->(value) { Time.at(value.to_i) }
+ coerce_result ->(value) { value.to_i }
+ end
+
+ named_entity_interface_type = GraphQL::InterfaceType.define do
+ name "NamedEntity"
+ field :name, !types.String
+ end
+
+ person_type = GraphQL::ObjectType.define do
+ name "Person"
+ interfaces [named_entity_interface_type]
+ field :name, !types.String
+ field :birthdate, timestamp_type
+ field :age, types.Int do
+ argument :on, !timestamp_type
+ resolve ->(obj, args, ctx) {
+ if obj.birthdate.nil?
+ nil
+ else
+ age_on = args[:on]
+ age_years = age_on.year - obj.birthdate.year
+ this_year_birthday = Time.new(age_on.year, obj.birthdate.month, obj.birthdate.day)
+ if this_year_birthday > age_on
+ age_years -= 1
+ end
+ end
+ age_years
+ }
+ end
+ field :organizations, types[organization_type] do
+ resolve ->(obj, args, ctx) {
+ obj.organization_ids.map { |id| DATA[id] }
+ }
+ end
+ field :first_organization, !organization_type do
+ resolve ->(obj, args, ctx) {
+ DATA[obj.organization_ids.first]
+ }
+ end
+ end
+
+ organization_type = GraphQL::ObjectType.define do
+ name "Organization"
+ interfaces [named_entity_interface_type]
+ field :name, !types.String
+ field :leader, !person_type do
+ resolve ->(obj, args, ctx) {
+ DATA[obj.leader_id] || (ctx[:return_error] ? ExecutionError.new("Error on Nullable") : nil)
+ }
+ end
+ field :returnedError, types.String do
+ resolve ->(o, a, c) {
+ GraphQL::ExecutionError.new("This error was returned")
+ }
+ end
+ field :raisedError, types.String do
+ resolve ->(o, a, c) {
+ raise GraphQL::ExecutionError.new("This error was raised")
+ }
+ end
+
+ field :nodePresence, !types[!types.Boolean] do
+ resolve ->(o, a, ctx) {
+ [
+ ctx.irep_node.is_a?(GraphQL::InternalRepresentation::Node),
+ ctx.ast_node.is_a?(GraphQL::Language::Nodes::AbstractNode),
+ false, # just testing
+ ]
+ }
+ end
+ end
+
+ node_union_type = GraphQL::UnionType.define do
+ name "Node"
+ possible_types [person_type, organization_type]
+ end
+
+ query_type = GraphQL::ObjectType.define do
+ name "Query"
+ field :node, node_union_type do
+ argument :id, !types.ID
+ resolve ->(obj, args, ctx) {
+ obj[args[:id]]
+ }
+ end
+
+ field :organization, !organization_type do
+ argument :id, !types.ID
+ resolve ->(obj, args, ctx) {
+ args[:id].start_with?("2") && obj[args[:id]]
+ }
+ end
+
+ field :organizations, types[organization_type] do
+ resolve ->(obj, args, ctx) {
+ [obj["2001"], obj["2002"]]
+ }
+ end
+ end
+
+ GraphQL::Schema.define do
+ query_execution_strategy execution_strategy
+ query query_type
+
+ resolve_type ->(obj, ctx) {
+ obj.respond_to?(:birthdate) ? person_type : organization_type
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/compatibility/lazy_execution_specification.rb b/lib/graphql/compatibility/lazy_execution_specification.rb
new file mode 100644
index 0000000000..1a2408fa28
--- /dev/null
+++ b/lib/graphql/compatibility/lazy_execution_specification.rb
@@ -0,0 +1,54 @@
+require "graphql/compatibility/lazy_execution_specification/lazy_schema"
+
+module GraphQL
+ module Compatibility
+ module LazyExecutionSpecification
+ # @param execution_strategy [<#new, #execute>] An execution strategy class
+ # @return [Class] A test suite for this execution strategy
+ def self.build_suite(execution_strategy)
+ Class.new(Minitest::Test) do
+ class << self
+ attr_accessor :lazy_schema
+ end
+
+ self.lazy_schema = LazySchema.build(execution_strategy)
+
+ def test_it_resolves_lazy_values
+ pushes = []
+ query_str = %|
+ {
+ p1: push(value: 1) {
+ value
+ }
+ p2: push(value: 2) {
+ push(value: 3) {
+ value
+ }
+ }
+ p3: push(value: 4) {
+ push(value: 5) {
+ value
+ }
+ }
+ }
+ |
+ res = self.class.lazy_schema.execute(query_str, context: {pushes: pushes})
+
+ expected_data = {
+ "p1"=>{"value"=>1},
+ "p2"=>{"push"=>{"value"=>3}},
+ "p3"=>{"push"=>{"value"=>5}},
+ }
+ assert_equal expected_data, res["data"]
+
+ expected_pushes = [
+ [1,2,4], # first level
+ [3,5], # second level
+ ]
+ assert_equal expected_pushes, pushes
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb b/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb
new file mode 100644
index 0000000000..ceef78c50a
--- /dev/null
+++ b/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb
@@ -0,0 +1,54 @@
+module GraphQL
+ module Compatibility
+ module LazyExecutionSpecification
+ module LazySchema
+ class LazyPush
+ attr_reader :value
+ def initialize(ctx, value)
+ @value = value
+ @context = ctx
+ pushes = @context[:lazy_pushes] ||= []
+ pushes << @value
+ end
+
+ def push
+ if @context[:lazy_pushes].include?(@value)
+ @context[:pushes] << @context[:lazy_pushes]
+ @context[:lazy_pushes] = []
+ end
+ self
+ end
+ end
+
+ def self.build(execution_strategy)
+ lazy_push_type = GraphQL::ObjectType.define do
+ name "LazyPush"
+ field :value, types.Int
+ field :push, lazy_push_type do
+ argument :value, types.Int
+ resolve ->(o, a, c) {
+ LazyPush.new(c, a[:value])
+ }
+ end
+ end
+
+ query_type = GraphQL::ObjectType.define do
+ name "Query"
+ field :push, lazy_push_type do
+ argument :value, types.Int
+ resolve ->(o, a, c) {
+ LazyPush.new(c, a[:value])
+ }
+ end
+ end
+
+ GraphQL::Schema.define do
+ query(query_type)
+ query_execution_strategy(execution_strategy)
+ lazy_resolve(LazyPush, :push)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/execution.rb b/lib/graphql/execution.rb
index c409a72235..8a7157c2cf 100644
--- a/lib/graphql/execution.rb
+++ b/lib/graphql/execution.rb
@@ -1,2 +1,6 @@
require "graphql/execution/directive_checks"
+require "graphql/execution/execute"
+require "graphql/execution/field_result"
+require "graphql/execution/lazy"
+require "graphql/execution/selection_result"
require "graphql/execution/typecast"
diff --git a/lib/graphql/execution/execute.rb b/lib/graphql/execution/execute.rb
new file mode 100644
index 0000000000..be6db118f5
--- /dev/null
+++ b/lib/graphql/execution/execute.rb
@@ -0,0 +1,219 @@
+module GraphQL
+ module Execution
+ # A valid execution strategy
+ class Execute
+ PROPAGATE_NULL = :__graphql_propagate_null__
+
+ def execute(ast_operation, root_type, query)
+ irep_root = query.internal_representation[ast_operation.name]
+
+ result = resolve_selection(
+ query.root_value,
+ root_type,
+ [irep_root],
+ query.context,
+ mutation: query.mutation?
+ )
+
+ GraphQL::Execution::Lazy.resolve(result)
+
+ result.to_h
+ end
+
+ private
+
+ def resolve_selection(object, current_type, irep_nodes, query_ctx, mutation: false )
+ query = query_ctx.query
+ own_selections = query.selections(irep_nodes, current_type)
+
+ selection_result = SelectionResult.new
+
+ own_selections.each do |name, child_irep_nodes|
+ field = query.get_field(current_type, child_irep_nodes.first.definition_name)
+ field_result = resolve_field(
+ selection_result,
+ child_irep_nodes,
+ current_type,
+ field,
+ object,
+ query_ctx
+ )
+
+ if mutation
+ GraphQL::Execution::Lazy.resolve(field_result)
+ end
+
+ selection_result.set(name, field_result)
+ end
+
+ selection_result
+ end
+
+ def resolve_field(owner, irep_nodes, parent_type, field, object, query_ctx)
+ irep_node = irep_nodes.first
+ query = query_ctx.query
+ field_ctx = query_ctx.spawn(
+ parent_type: parent_type,
+ field: field,
+ path: query_ctx.path + [irep_node.name],
+ irep_node: irep_node,
+ irep_nodes: irep_nodes,
+ )
+
+ arguments = query.arguments_for(irep_node, field)
+ middlewares = query.schema.middleware
+ resolve_arguments = [parent_type, object, field, arguments, field_ctx]
+
+ raw_value = begin
+ # only run a middleware chain if there are any middleware
+ if middlewares.any?
+ chain = GraphQL::Schema::MiddlewareChain.new(
+ steps: middlewares + [FieldResolveStep],
+ arguments: resolve_arguments
+ )
+ chain.call
+ else
+ FieldResolveStep.call(*resolve_arguments)
+ end
+ rescue GraphQL::ExecutionError => err
+ err
+ end
+
+ lazy_method_name = query.lazy_method(raw_value)
+ result = if lazy_method_name
+ GraphQL::Execution::Lazy.new { raw_value.public_send(lazy_method_name) }.then { |inner_value|
+ continue_resolve_field(irep_nodes, parent_type, field, inner_value, field_ctx)
+ }
+ else
+ continue_resolve_field(irep_nodes, parent_type, field, raw_value, field_ctx)
+ end
+
+ FieldResult.new(
+ owner: owner,
+ field: field,
+ value: result,
+ )
+ end
+
+ def continue_resolve_field(irep_nodes, parent_type, field, raw_value, field_ctx)
+ irep_node = irep_nodes.first
+ query = field_ctx.query
+
+ case raw_value
+ when GraphQL::ExecutionError
+ raw_value.ast_node = irep_node.ast_node
+ raw_value.path = field_ctx.path
+ query.context.errors.push(raw_value)
+ when Array
+ list_errors = raw_value.each_with_index.select { |value, _| value.is_a?(GraphQL::ExecutionError) }
+ if list_errors.any?
+ list_errors.each do |error, index|
+ error.ast_node = irep_node.ast_node
+ error.path = field_ctx.path + [index]
+ query.context.errors.push(error)
+ end
+ end
+ end
+
+ resolve_value(
+ parent_type,
+ field,
+ field.type,
+ raw_value,
+ irep_nodes,
+ field_ctx,
+ )
+ end
+
+ def resolve_value(parent_type, field_defn, field_type, value, irep_nodes, field_ctx)
+ if value.nil?
+ if field_type.kind.non_null?
+ field_ctx.add_error(GraphQL::ExecutionError.new("Cannot return null for non-nullable field #{parent_type.name}.#{field_defn.name}"))
+ PROPAGATE_NULL
+ else
+ nil
+ end
+ elsif value.is_a?(GraphQL::ExecutionError)
+ if field_type.kind.non_null?
+ PROPAGATE_NULL
+ else
+ nil
+ end
+ else
+ case field_type.kind
+ when GraphQL::TypeKinds::SCALAR
+ field_type.coerce_result(value)
+ when GraphQL::TypeKinds::ENUM
+ field_type.coerce_result(value, field_ctx.query.warden)
+ when GraphQL::TypeKinds::LIST
+ wrapped_type = field_type.of_type
+ result = value.each_with_index.map do |inner_value, index|
+ inner_ctx = field_ctx.spawn(
+ path: field_ctx.path + [index],
+ irep_node: field_ctx.irep_node,
+ irep_nodes: irep_nodes,
+ parent_type: parent_type,
+ field: field_defn,
+ )
+
+ inner_result = resolve_value(
+ parent_type,
+ field_defn,
+ wrapped_type,
+ inner_value,
+ irep_nodes,
+ inner_ctx,
+ )
+ inner_result
+ end
+ result
+ when GraphQL::TypeKinds::NON_NULL
+ wrapped_type = field_type.of_type
+ resolve_value(
+ parent_type,
+ field_defn,
+ wrapped_type,
+ value,
+ irep_nodes,
+ field_ctx,
+ )
+ when GraphQL::TypeKinds::OBJECT
+ resolve_selection(
+ value,
+ field_type,
+ irep_nodes,
+ field_ctx
+ )
+ when GraphQL::TypeKinds::UNION, GraphQL::TypeKinds::INTERFACE
+ query = field_ctx.query
+ resolved_type = query.resolve_type(value)
+ possible_types = query.possible_types(field_type)
+
+ if !possible_types.include?(resolved_type)
+ raise GraphQL::UnresolvedTypeError.new(irep_nodes.first.definition_name, field_type, parent_type, resolved_type, possible_types)
+ else
+ resolve_value(
+ parent_type,
+ field_defn,
+ resolved_type,
+ value,
+ irep_nodes,
+ field_ctx,
+ )
+ end
+ else
+ raise("Unknown type kind: #{field_type.kind}")
+ end
+ end
+ end
+
+ # A `.call`-able suitable to be the last step in a middleware chain
+ module FieldResolveStep
+ # Execute the field's resolve method
+ def self.call(_parent_type, parent_object, field_definition, field_args, context, _next = nil)
+ field_definition.resolve(parent_object, field_args, context)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/execution/field_result.rb b/lib/graphql/execution/field_result.rb
new file mode 100644
index 0000000000..c49ba9f414
--- /dev/null
+++ b/lib/graphql/execution/field_result.rb
@@ -0,0 +1,51 @@
+module GraphQL
+ module Execution
+ # This is one key-value pair in a GraphQL response.
+ class FieldResult
+ # @return [Any, Lazy] the GraphQL-ready response value, or a {Lazy} instance
+ attr_reader :value
+
+ # @return [GraphQL::Field] The field which resolved this value
+ attr_reader :field
+
+ # @return [SelectionResult] The result object that this field belongs to
+ attr_reader :owner
+
+ def initialize(field:, value:, owner:)
+ @field = field
+ @owner = owner
+ self.value = value
+ end
+
+ # Set a new value for this field in the response.
+ # It may be updated after resolving a {Lazy}.
+ # If it is {Execute::PROPAGATE_NULL}, tell the owner to propagate null.
+ # If the value is a {SelectionResult}, make a link with it, and if it's already null,
+ # propagate the null as needed.
+ # @param new_value [Any] The GraphQL-ready value
+ def value=(new_value)
+ if new_value.is_a?(SelectionResult)
+ if new_value.invalid_null?
+ new_value = GraphQL::Execution::Execute::PROPAGATE_NULL
+ else
+ new_value.owner = self
+ end
+ end
+
+ if new_value == GraphQL::Execution::Execute::PROPAGATE_NULL
+ if field.type.kind.non_null?
+ @owner.propagate_null
+ else
+ @value = nil
+ end
+ else
+ @value = new_value
+ end
+ end
+
+ def inspect
+ "#"
+ end
+ end
+ end
+end
diff --git a/lib/graphql/execution/lazy.rb b/lib/graphql/execution/lazy.rb
new file mode 100644
index 0000000000..6b7323aee0
--- /dev/null
+++ b/lib/graphql/execution/lazy.rb
@@ -0,0 +1,47 @@
+require "graphql/execution/lazy/lazy_method_map"
+require "graphql/execution/lazy/resolve"
+module GraphQL
+ module Execution
+ # This wraps a value which is available, but not yet calculated, like a promise or future.
+ #
+ # Calling `#value` will trigger calculation & return the "lazy" value.
+ #
+ # This is an itty-bitty promise-like object, with key differences:
+ # - It has only two states, not-resolved and resolved
+ # - It has no error-catching functionality
+ class Lazy
+ # Traverse `val`, lazily resolving any values along the way
+ # @param val [Object] A data structure containing mixed plain values and `Lazy` instances
+ # @return void
+ def self.resolve(val)
+ Resolve.resolve(val)
+ end
+
+ # Create a {Lazy} which will get its inner value by calling the block
+ # @param get_value_func [Proc] a block to get the inner value (later)
+ def initialize(&get_value_func)
+ @get_value_func = get_value_func
+ @resolved = false
+ end
+
+ # @return [Object] The wrapped value, calling the lazy block if necessary
+ def value
+ if !@resolved
+ @resolved = true
+ @value = @get_value_func.call
+ end
+ @value
+ rescue GraphQL::ExecutionError => err
+ @resolved = true
+ @value = err
+ end
+
+ # @return [Lazy] A {Lazy} whose value depends on another {Lazy}, plus any transformations in `block`
+ def then(&block)
+ self.class.new {
+ next_val = block.call(value)
+ }
+ end
+ end
+ end
+end
diff --git a/lib/graphql/execution/lazy/lazy_method_map.rb b/lib/graphql/execution/lazy/lazy_method_map.rb
new file mode 100644
index 0000000000..2f51b68d09
--- /dev/null
+++ b/lib/graphql/execution/lazy/lazy_method_map.rb
@@ -0,0 +1,32 @@
+module GraphQL
+ module Execution
+ class Lazy
+ # {GraphQL::Schema} uses this to match returned values to lazy resolution methods.
+ # Methods may be registered for classes, they apply to its subclasses also.
+ # The result of this lookup is cached for future resolutions.
+ class LazyMethodMap
+ def initialize
+ @storage = {}
+ end
+
+ # @param lazy_class [Class] A class which represents a lazy value (subclasses may also be used)
+ # @param lazy_value_method [Symbol] The method to call on this class to get its value
+ def set(lazy_class, lazy_value_method)
+ @storage[lazy_class] = lazy_value_method
+ end
+
+ # @param value [Object] an object which may have a `lazy_value_method` registered for its class or superclasses
+ # @return [Symbol, nil] The `lazy_value_method` for this object, or nil
+ def get(value)
+ if @storage.key?(value.class)
+ @storage[value.class]
+ else
+ value_class = value.class
+ registered_superclass = @storage.each_key.find { |lazy_class| value_class < lazy_class }
+ @storage[value_class] = @storage[registered_superclass]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/execution/lazy/resolve.rb b/lib/graphql/execution/lazy/resolve.rb
new file mode 100644
index 0000000000..25a41a69a1
--- /dev/null
+++ b/lib/graphql/execution/lazy/resolve.rb
@@ -0,0 +1,67 @@
+module GraphQL
+ module Execution
+ class Lazy
+ # Helpers for dealing with data structures containing {Lazy} instances
+ module Resolve
+ # Mutate `value`, replacing {Lazy} instances in place with their resolved values
+ # @return [void]
+ def self.resolve(value)
+ lazies = resolve_in_place(value)
+ deep_sync(lazies)
+ end
+
+ def self.resolve_in_place(value)
+ lazies = []
+
+ each_lazy(value) do |field_result|
+ inner_lazy = field_result.value.then do |inner_v|
+ field_result.value = inner_v
+ resolve_in_place(inner_v)
+ end
+ lazies.push(inner_lazy)
+ end
+
+ Lazy.new { lazies.map(&:value) }
+ end
+
+ # If `value` is a collection, call `block`
+ # with any {Lazy} instances in the collection
+ # @return [void]
+ def self.each_lazy(value, &block)
+ case value
+ when SelectionResult
+ value.each do |key, field_result|
+ each_lazy(field_result, &block)
+ end
+ when Array
+ value.each do |field_result|
+ each_lazy(field_result, &block)
+ end
+ when FieldResult
+ field_value = value.value
+ if field_value.is_a?(Lazy)
+ yield(value)
+ else
+ each_lazy(field_value, &block)
+ end
+ end
+ end
+
+ # Traverse `val`, triggering resolution for each {Lazy}.
+ # These {Lazy}s are expected to mutate their owner data structures
+ # during resolution! (They're created with the `.then` calls in `resolve_in_place`).
+ # @return [void]
+ def self.deep_sync(val)
+ case val
+ when Lazy
+ deep_sync(val.value)
+ when Array
+ val.each { |v| deep_sync(v.value) }
+ when Hash
+ val.each { |k, v| deep_sync(v.value) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/execution/selection_result.rb b/lib/graphql/execution/selection_result.rb
new file mode 100644
index 0000000000..d876479284
--- /dev/null
+++ b/lib/graphql/execution/selection_result.rb
@@ -0,0 +1,83 @@
+module GraphQL
+ module Execution
+ # A set of key-value pairs suitable for a GraphQL response.
+ class SelectionResult
+ def initialize
+ @storage = {}
+ @owner = nil
+ @invalid_null = false
+ end
+
+ # @param key [String] The name for this value in the result
+ # @param field_result [FieldResult] The result for this field
+ def set(key, field_result)
+ @storage[key] = field_result
+ end
+
+ # @param key [String] The name of an already-defined result
+ # @return [FieldResult] The result for this field
+ def fetch(key)
+ @storage.fetch(key)
+ end
+
+ # Visit each key-result pair in this result
+ def each
+ @storage.each do |key, field_res|
+ yield(key, field_res)
+ end
+ end
+
+ # @return [Hash] A plain Hash representation of this result
+ def to_h
+ if @invalid_null
+ nil
+ else
+ flatten(self)
+ end
+ end
+
+ # A field has been unexpectedly nullified.
+ # Tell the owner {FieldResult} if it is present.
+ # Record {#invalid_null} in case an owner is added later.
+ def propagate_null
+ if @owner
+ @owner.value = GraphQL::Execution::Execute::PROPAGATE_NULL
+ end
+ @invalid_null = true
+ end
+
+ # @return [Boolean] True if this selection has been nullified by a null child
+ def invalid_null?
+ @invalid_null
+ end
+
+ # @param field_result [FieldResult] The field that this selection belongs to (used for propagating nulls)
+ def owner=(field_result)
+ if @owner
+ raise("Can't change owners of SelectionResult")
+ else
+ @owner = field_result
+ end
+ end
+
+ private
+
+ def flatten(obj)
+ case obj
+ when SelectionResult
+ flattened = {}
+ obj.each do |key, val|
+ flattened[key] = flatten(val)
+ end
+ flattened
+ when Array
+ obj.map { |v| flatten(v) }
+ when FieldResult
+ flatten(obj.value)
+ else
+ obj
+ end
+ end
+ end
+ end
+end
diff --git a/lib/graphql/query.rb b/lib/graphql/query.rb
index 99a39d2041..8c91badc0c 100644
--- a/lib/graphql/query.rb
+++ b/lib/graphql/query.rb
@@ -82,12 +82,14 @@ def initialize(schema, query_string = nil, document: nil, context: nil, variable
# Trying to execute a document
# with no operations returns an empty hash
@ast_variables = []
+ @mutation = false
if @operations.any?
@selected_operation = find_operation(@operations, @operation_name)
if @selected_operation.nil?
@validation_errors << GraphQL::Query::OperationNameMissingError.new(@operations.keys)
else
@ast_variables = @selected_operation.variables
+ @mutation = @selected_operation.operation_type == "mutation"
end
end
end
@@ -192,6 +194,14 @@ def resolve_type(type)
@schema.resolve_type(type, @context)
end
+ def lazy_method(value)
+ @schema.lazy_methods.get(value)
+ end
+
+ def mutation?
+ @mutation
+ end
+
private
# Assert that the passed-in query string is internally consistent
diff --git a/lib/graphql/query/context.rb b/lib/graphql/query/context.rb
index d7ed9922f7..78bd968956 100644
--- a/lib/graphql/query/context.rb
+++ b/lib/graphql/query/context.rb
@@ -59,19 +59,29 @@ def []=(key, value)
@values[key] = value
end
- def spawn(path:, irep_node:)
- FieldResolutionContext.new(context: self, path: path, irep_node: irep_node)
+ def spawn(path:, irep_node:, parent_type:, field:, irep_nodes:)
+ FieldResolutionContext.new(
+ context: self,
+ path: path,
+ irep_node: irep_node,
+ parent_type: parent_type,
+ field: field,
+ irep_nodes: irep_nodes,
+ )
end
class FieldResolutionContext
extend Forwardable
- attr_reader :path, :irep_node
+ attr_reader :path, :irep_node, :field, :parent_type, :irep_nodes
- def initialize(context:, path:, irep_node:)
+ def initialize(context:, path:, irep_node:, field:, parent_type:, irep_nodes:)
@context = context
@path = path
@irep_node = irep_node
+ @field = field
+ @parent_type = parent_type
+ @irep_nodes = irep_nodes
end
def_delegators :@context, :[], :[]=, :spawn, :query, :schema, :warden, :errors, :execution_strategy, :strategy
@@ -85,7 +95,7 @@ def ast_node
# @param error [GraphQL::ExecutionError] an execution error
# @return [void]
def add_error(error)
- unless error.is_a?(ExecutionError)
+ if !error.is_a?(ExecutionError)
raise TypeError, "expected error to be a ExecutionError, but was #{error.class}"
end
diff --git a/lib/graphql/query/serial_execution/field_resolution.rb b/lib/graphql/query/serial_execution/field_resolution.rb
index e241816e27..2c0b12d628 100644
--- a/lib/graphql/query/serial_execution/field_resolution.rb
+++ b/lib/graphql/query/serial_execution/field_resolution.rb
@@ -9,9 +9,15 @@ def initialize(irep_nodes, parent_type, target, query_ctx)
@irep_nodes = irep_nodes
@parent_type = parent_type
@target = target
- @field_ctx = query_ctx.spawn(path: query_ctx.path + [irep_node.name], irep_node: irep_node)
@query = query_ctx.query
@field = @query.get_field(parent_type, irep_node.definition_name)
+ @field_ctx = query_ctx.spawn(
+ path: query_ctx.path + [irep_node.name],
+ irep_node: irep_node,
+ parent_type: parent_type,
+ field: field,
+ irep_nodes: irep_nodes
+ )
@arguments = @query.arguments_for(irep_node, @field)
end
diff --git a/lib/graphql/query/serial_execution/value_resolution.rb b/lib/graphql/query/serial_execution/value_resolution.rb
index 8925a48e38..7e547fd1a8 100644
--- a/lib/graphql/query/serial_execution/value_resolution.rb
+++ b/lib/graphql/query/serial_execution/value_resolution.rb
@@ -18,7 +18,14 @@ def self.resolve(parent_type, field_defn, field_type, value, irep_nodes, query_c
when GraphQL::TypeKinds::LIST
wrapped_type = field_type.of_type
result = value.each_with_index.map do |inner_value, index|
- inner_ctx = query_ctx.spawn(path: query_ctx.path + [index], irep_node: query_ctx.irep_node)
+ inner_ctx = query_ctx.spawn(
+ path: query_ctx.path + [index],
+ irep_node: query_ctx.irep_node,
+ parent_type: wrapped_type,
+ field: field_defn,
+ irep_nodes: irep_nodes,
+ )
+
inner_result = resolve(
parent_type,
field_defn,
diff --git a/lib/graphql/schema.rb b/lib/graphql/schema.rb
index f819238c51..92998b075c 100644
--- a/lib/graphql/schema.rb
+++ b/lib/graphql/schema.rb
@@ -57,6 +57,7 @@ class Schema
instrument: -> (schema, type, instrumenter) { schema.instrumenters[type] << instrumenter },
query_analyzer: ->(schema, analyzer) { schema.query_analyzers << analyzer },
middleware: ->(schema, middleware) { schema.middleware << middleware },
+ lazy_resolve: ->(schema, lazy_class, lazy_value_method) { schema.lazy_methods.set(lazy_class, lazy_value_method) },
rescue_from: ->(schema, err_class, &block) { schema.rescue_from(err_class, &block)}
attr_accessor \
@@ -64,7 +65,13 @@ class Schema
:query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
:max_depth, :max_complexity,
:orphan_types, :directives,
- :query_analyzers, :middleware, :instrumenters
+ :query_analyzers, :middleware, :instrumenters, :lazy_methods
+
+ class << self
+ attr_accessor :default_execution_strategy
+ end
+
+ self.default_execution_strategy = GraphQL::Execution::Execute
BUILT_IN_TYPES = Hash[[INT_TYPE, STRING_TYPE, FLOAT_TYPE, BOOLEAN_TYPE, ID_TYPE].map{ |type| [type.name, type] }]
DIRECTIVES = [GraphQL::Directive::IncludeDirective, GraphQL::Directive::SkipDirective, GraphQL::Directive::DeprecatedDirective]
@@ -90,10 +97,11 @@ def initialize
@object_from_id_proc = nil
@id_from_object_proc = nil
@instrumenters = Hash.new { |h, k| h[k] = [] }
+ @lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new
# Default to the built-in execution strategy:
- @query_execution_strategy = GraphQL::Query::SerialExecution
- @mutation_execution_strategy = GraphQL::Query::SerialExecution
- @subscription_execution_strategy = GraphQL::Query::SerialExecution
+ @query_execution_strategy = self.class.default_execution_strategy
+ @mutation_execution_strategy = self.class.default_execution_strategy
+ @subscription_execution_strategy = self.class.default_execution_strategy
end
def rescue_from(*args, &block)
diff --git a/spec/graphql/compatibility/lazy_execution_specification_spec.rb b/spec/graphql/compatibility/lazy_execution_specification_spec.rb
new file mode 100644
index 0000000000..8b2c09fefd
--- /dev/null
+++ b/spec/graphql/compatibility/lazy_execution_specification_spec.rb
@@ -0,0 +1,3 @@
+require "spec_helper"
+
+LazySpecSuite = GraphQL::Compatibility::LazyExecutionSpecification.build_suite(GraphQL::Execution::Execute)
diff --git a/spec/graphql/execution/execute_spec.rb b/spec/graphql/execution/execute_spec.rb
new file mode 100644
index 0000000000..4b33c9ccff
--- /dev/null
+++ b/spec/graphql/execution/execute_spec.rb
@@ -0,0 +1,4 @@
+require "spec_helper"
+
+ExecuteSuite = GraphQL::Compatibility::ExecutionSpecification.build_suite(GraphQL::Execution::Execute)
+LazyExecuteSuite = GraphQL::Compatibility::LazyExecutionSpecification.build_suite(GraphQL::Execution::Execute)
diff --git a/spec/graphql/execution/lazy_spec.rb b/spec/graphql/execution/lazy_spec.rb
new file mode 100644
index 0000000000..337d1b400f
--- /dev/null
+++ b/spec/graphql/execution/lazy_spec.rb
@@ -0,0 +1,249 @@
+require "spec_helper"
+
+describe GraphQL::Execution::Lazy do
+ class Wrapper
+ def initialize(item = nil, &block)
+ if block
+ @block = block
+ else
+ @item = item
+ end
+ end
+
+ def item
+ if @block
+ @item = @block.call()
+ @block = nil
+ end
+ @item
+ end
+ end
+
+ class SumAll
+ attr_reader :own_value
+ attr_accessor :value
+
+ def initialize(ctx, own_value)
+ @own_value = own_value
+ @all = ctx[:__sum_all__] ||= []
+ @all << self
+ end
+
+ def value
+ @value ||= begin
+ total_value = @all.map(&:own_value).reduce(&:+)
+ @all.each { |v| v.value = total_value}
+ @all.clear
+ total_value
+ end
+ @value
+ end
+ end
+
+ LazySum = GraphQL::ObjectType.define do
+ name "LazySum"
+ field :value, types.Int do
+ resolve ->(o, a, c) { o == 13 ? nil : o }
+ end
+ field :nestedSum, !LazySum do
+ argument :value, !types.Int
+ resolve ->(o, args, c) {
+ if args[:value] == 13
+ Wrapper.new(nil)
+ else
+ SumAll.new(c, o + args[:value])
+ end
+ }
+ end
+
+ field :nullableNestedSum, LazySum do
+ argument :value, types.Int
+ resolve ->(o, args, c) {
+ if args[:value] == 13
+ Wrapper.new(nil)
+ else
+ SumAll.new(c, o + args[:value])
+ end
+ }
+ end
+ end
+
+ LazyQuery = GraphQL::ObjectType.define do
+ name "Query"
+ field :int, !types.Int do
+ argument :value, !types.Int
+ argument :plus, types.Int, default_value: 0
+ resolve ->(o, a, c) { Wrapper.new(a[:value] + a[:plus])}
+ end
+
+ field :nestedSum, !LazySum do
+ argument :value, !types.Int
+ resolve ->(o, args, c) { SumAll.new(c, args[:value]) }
+ end
+
+ field :nullableNestedSum, LazySum do
+ argument :value, types.Int
+ resolve ->(o, args, c) {
+ if args[:value] == 13
+ Wrapper.new { raise GraphQL::ExecutionError.new("13 is unlucky") }
+ else
+ SumAll.new(c, args[:value])
+ end
+ }
+ end
+
+ field :listSum, types[LazySum] do
+ argument :values, types[types.Int]
+ resolve ->(o, args, c) { args[:values] }
+ end
+ end
+
+ LazySchema = GraphQL::Schema.define do
+ query(LazyQuery)
+ mutation(LazyQuery)
+ lazy_resolve(Wrapper, :item)
+ lazy_resolve(SumAll, :value)
+ end
+
+ def run_query(query_str)
+ LazySchema.execute(query_str)
+ end
+
+ describe "resolving" do
+ it "calls value handlers" do
+ res = run_query('{ int(value: 2, plus: 1)}')
+ assert_equal 3, res["data"]["int"]
+ end
+
+ it "can do nested lazy values" do
+ res = run_query %|
+ {
+ a: nestedSum(value: 3) {
+ value
+ nestedSum(value: 7) {
+ value
+ }
+ }
+ b: nestedSum(value: 2) {
+ value
+ nestedSum(value: 11) {
+ value
+ }
+ }
+
+ c: listSum(values: [1,2]) {
+ nestedSum(value: 3) {
+ value
+ }
+ }
+ }
+ |
+
+ expected_data = {
+ "a"=>{"value"=>14, "nestedSum"=>{"value"=>46}},
+ "b"=>{"value"=>14, "nestedSum"=>{"value"=>46}},
+ "c"=>[{"nestedSum"=>{"value"=>14}}, {"nestedSum"=>{"value"=>14}}],
+ }
+
+ assert_equal expected_data, res["data"]
+ end
+
+ it "propagates nulls" do
+ res = run_query %|
+ {
+ nestedSum(value: 1) {
+ value
+ nestedSum(value: 13) {
+ value
+ }
+ }
+ }|
+
+ assert_equal(nil, res["data"])
+ assert_equal 1, res["errors"].length
+
+
+ res = run_query %|
+ {
+ nullableNestedSum(value: 1) {
+ value
+ nullableNestedSum(value: 2) {
+ nestedSum(value: 13) {
+ value
+ }
+ }
+ }
+ }|
+
+ expected_data = {
+ "nullableNestedSum" => {
+ "value" => 1,
+ "nullableNestedSum" => nil,
+ }
+ }
+ assert_equal(expected_data, res["data"])
+ assert_equal 1, res["errors"].length
+ end
+
+ it "handles raised errors" do
+ res = run_query %|
+ {
+ a: nullableNestedSum(value: 1) { value }
+ b: nullableNestedSum(value: 13) { value }
+ c: nullableNestedSum(value: 2) { value }
+ }|
+
+ expected_data = {
+ "a" => { "value" => 3 },
+ "b" => nil,
+ "c" => { "value" => 3 },
+ }
+ assert_equal expected_data, res["data"]
+
+ expected_errors = [{
+ "message"=>"13 is unlucky",
+ "locations"=>[{"line"=>4, "column"=>9}],
+ "path"=>["b"],
+ }]
+ assert_equal expected_errors, res["errors"]
+ end
+
+ it "resolves mutation fields right away" do
+ res = run_query %|
+ {
+ a: nestedSum(value: 2) { value }
+ b: nestedSum(value: 4) { value }
+ c: nestedSum(value: 6) { value }
+ }|
+
+ assert_equal [12, 12, 12], res["data"].values.map { |d| d["value"] }
+
+ res = run_query %|
+ mutation {
+ a: nestedSum(value: 2) { value }
+ b: nestedSum(value: 4) { value }
+ c: nestedSum(value: 6) { value }
+ }
+ |
+
+ assert_equal [2, 4, 6], res["data"].values.map { |d| d["value"] }
+ end
+ end
+
+ describe "LazyMethodMap" do
+ class SubWrapper < Wrapper; end
+
+ let(:map) { GraphQL::Execution::Lazy::LazyMethodMap.new }
+
+ it "finds methods for classes and subclasses" do
+ map.set(Wrapper, :item)
+ map.set(SumAll, :value)
+ b = Wrapper.new(1)
+ sub_b = Wrapper.new(2)
+ s = SumAll.new({}, 3)
+ assert_equal(:item, map.get(b))
+ assert_equal(:item, map.get(sub_b))
+ assert_equal(:value, map.get(s))
+ end
+ end
+end
diff --git a/spec/graphql/non_null_type_spec.rb b/spec/graphql/non_null_type_spec.rb
index 6da5d1fa69..edd9c547aa 100644
--- a/spec/graphql/non_null_type_spec.rb
+++ b/spec/graphql/non_null_type_spec.rb
@@ -6,7 +6,11 @@
query_string = %|{ cow { name cantBeNullButIs } }|
result = DummySchema.execute(query_string)
assert_equal({"cow" => nil }, result["data"])
- assert_equal([{"message"=>"Cannot return null for non-nullable field Cow.cantBeNullButIs"}], result["errors"])
+ assert_equal([{
+ "message"=>"Cannot return null for non-nullable field Cow.cantBeNullButIs",
+ "locations"=>[{"line"=>1, "column"=>14}],
+ "path"=>["cow", "cantBeNullButIs"],
+ }], result["errors"])
end
it "propagates the null up to the next nullable field" do
@@ -25,7 +29,11 @@
|
result = DummySchema.execute(query_string)
assert_equal(nil, result["data"])
- assert_equal([{"message"=>"Cannot return null for non-nullable field DeepNonNull.nonNullInt"}], result["errors"])
+ assert_equal([{
+ "message"=>"Cannot return null for non-nullable field DeepNonNull.nonNullInt",
+ "locations"=>[{"line"=>8, "column"=>15}],
+ "path"=>["nn1", "nn2", "nn3", "nni3"],
+ }], result["errors"])
end
end
end
diff --git a/spec/graphql/query/executor_spec.rb b/spec/graphql/query/executor_spec.rb
index adf74ec452..a802ce705a 100644
--- a/spec/graphql/query/executor_spec.rb
+++ b/spec/graphql/query/executor_spec.rb
@@ -127,7 +127,9 @@
"data" => { "cow" => nil },
"errors" => [
{
- "message" => "Cannot return null for non-nullable field Cow.cantBeNullButIs"
+ "message" => "Cannot return null for non-nullable field Cow.cantBeNullButIs",
+ "locations"=>[{"line"=>1, "column"=>28}],
+ "path"=>["cow", "cantBeNullButIs"],
}
]
}