Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 52 additions & 42 deletions lib/graphql/cardinal/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require_relative "./executor/execution_field"
require_relative "./executor/authorization"
require_relative "./executor/hot_paths"
require_relative "./executor/response_hash"
require_relative "./executor/result_hash"
require_relative "./executor/error_formatter"

module GraphQL
Expand Down Expand Up @@ -42,8 +42,8 @@ def perform
ExecutionScope.new(
parent_type: @query.root_type_for_operation(operation.operation_type),
selections: operation.selections,
sources: [@root_object],
responses: [@data],
objects: [@root_object],
results: [@data],
)
]
when "mutation"
Expand All @@ -53,8 +53,8 @@ def perform
ExecutionScope.new(
parent_type: mutation_type,
selections: exec_field.nodes,
sources: [@root_object],
responses: [@data],
objects: [@root_object],
results: [@data],
)
end
else
Expand All @@ -67,9 +67,9 @@ def perform
execute_scope(@exec_queue.shift) until @exec_queue.empty?
end

response = { "data" => @errors.empty? ? @data : ErrorFormatter.new(@query, @data, @errors).perform }
response["errors"] = @errors.map(&:to_h) unless @errors.empty?
response
result = { "data" => @errors.empty? ? @data : ErrorFormatter.new(@query, @data, @errors).perform }
result["errors"] = @errors.map(&:to_h) unless @errors.empty?
result
end

private
Expand All @@ -79,7 +79,7 @@ def execute_scope(exec_scope)
exec_scope.fields = execution_fields_by_key(exec_scope.parent_type, exec_scope.selections)
exec_scope.fields.each_value do |exec_field|
parent_type = exec_scope.parent_type
parent_sources = exec_scope.sources
parent_objects = exec_scope.objects
field_name = exec_field.name

exec_field.scope = exec_scope
Expand All @@ -97,20 +97,20 @@ def execute_scope(exec_scope)

exec_field.result = if !field_resolver.authorized?(@context)
@errors << AuthorizationError.new(type_name: parent_type.graphql_name, field_name: field_name, path: exec_field.path, base: true)
Array.new(parent_sources.length, @errors.last)
Array.new(parent_objects.length, @errors.last)
elsif !Authorization.can_access_type?(value_type, @context)
@errors << AuthorizationError.new(type_name: value_type.graphql_name, path: exec_field.path, base: true)
Array.new(parent_sources.length, @errors.last)
Array.new(parent_objects.length, @errors.last)
else
begin
@tracers.each { _1.before_resolve_field(parent_type, field_name, parent_sources.length, @context) }
field_resolver.resolve(parent_sources, exec_field.arguments(@variables), @context, exec_scope)
@tracers.each { _1.before_resolve_field(parent_type, field_name, parent_objects.length, @context) }
field_resolver.resolve(parent_objects, exec_field.arguments(@variables), @context, exec_scope)
rescue StandardError => e
report_exception(error: e, field: exec_field)
@errors << InternalError.new(path: exec_field.path, base: true)
Array.new(parent_sources.length, @errors.last)
Array.new(parent_objects.length, @errors.last)
ensure
@tracers.each { _1.after_resolve_field(parent_type, field_name, parent_sources.length, @context) }
@tracers.each { _1.after_resolve_field(parent_type, field_name, parent_objects.length, @context) }
@exec_count += 1
end
end
Expand All @@ -121,41 +121,44 @@ def execute_scope(exec_scope)
if exec_scope.lazy_fields_ready?
exec_scope.send(:lazy_exec!) # << noop for loaders that have already run
exec_scope.fields.each_value do |exec_field|
sources = exec_field.result.is_a?(Promise) ? exec_field.result.value : exec_field.result
resolve_execution_field(exec_field, sources)
objects = exec_field.result.is_a?(Promise) ? exec_field.result.value : exec_field.result
build_execution_field(exec_field, objects)
end
else
# requeue the scope to wait on others that haven't built fields yet
@exec_queue << exec_scope
end
else
exec_scope.fields.each_value do |exec_field|
resolve_execution_field(exec_field, exec_field.result)
build_execution_field(exec_field, exec_field.result)
end
end

nil
end

def resolve_execution_field(exec_field, resolved_sources)
parent_sources = exec_field.scope.sources
parent_responses = exec_field.scope.responses
def build_execution_field(exec_field, resolved_objects)
parent_objects = exec_field.scope.objects
parent_results = exec_field.scope.results
field_key = exec_field.key
field_type = exec_field.type
return_type = field_type.unwrap

if resolved_sources.length != parent_sources.length
report_exception("Incorrect number of results resolved. Expected #{parent_sources.length}, got #{resolved_sources.length}", field: exec_field)
resolved_sources = Array.new(parent_sources.length, nil)
if resolved_objects.length != parent_objects.length
report_exception("Incorrect number of results resolved. Expected #{parent_objects.length}, got #{resolved_objects.length}", field: exec_field)
resolved_objects = Array.new(parent_objects.length, nil)
end

if return_type.kind.composite?
# build results with child selections
next_sources = []
next_responses = []
resolved_sources.each_with_index do |source, i|
next_objects = []
next_results = []
i = 0
while i < resolved_objects.length
# DANGER: HOT PATH!
parent_responses[i][field_key] = build_composite_response(exec_field, field_type, source, next_sources, next_responses)
object = resolved_objects[i]
parent_results[i][field_key] = build_composite_result(exec_field, field_type, object, next_objects, next_results)
i += 1
end

if return_type.kind.abstract?
Expand All @@ -164,29 +167,33 @@ def resolve_execution_field(exec_field, resolved_sources)
raise NotImplementedError, "No type resolver for `#{return_type.graphql_name}`"
end

next_sources_by_type = Hash.new { |h, k| h[k] = [] }
next_responses_by_type = Hash.new { |h, k| h[k] = [] }
next_sources.each_with_index do |source, i|
next_objects_by_type = Hash.new { |h, k| h[k] = [] }
next_results_by_type = Hash.new { |h, k| h[k] = [] }

i = 0
while i < next_objects.length
# DANGER: HOT PATH!
impl_type = type_resolver.call(source, @context)
next_sources_by_type[impl_type] << (exec_field.name == TYPENAME_FIELD ? impl_type.graphql_name : source)
next_responses_by_type[impl_type] << next_responses[i].tap { |r| r.typename = impl_type.graphql_name }
object = next_objects[i]
impl_type = type_resolver.call(object, @context)
next_objects_by_type[impl_type] << (exec_field.name == TYPENAME_FIELD ? impl_type.graphql_name : object)
next_results_by_type[impl_type] << next_results[i].tap { |r| r.typename = impl_type.graphql_name }
i += 1
end

loader_cache = {} # << all scopes in the abstract generation share a loader cache
loader_group = []
next_sources_by_type.each do |impl_type, impl_type_sources|
next_objects_by_type.each do |impl_type, impl_type_objects|
# check concrete type access only once per resolved type...
unless Authorization.can_access_type?(impl_type, @context)
@errors << AuthorizationError.new(type_name: impl_type.graphql_name, path: exec_field.path, base: true)
impl_type_sources = Array.new(impl_type_sources.length, @errors.last)
impl_type_objects = Array.new(impl_type_objects.length, @errors.last)
end

loader_group << ExecutionScope.new(
parent_type: impl_type,
selections: exec_field.selections,
sources: impl_type_sources,
responses: next_responses_by_type[impl_type],
objects: impl_type_objects,
results: next_results_by_type[impl_type],
loader_cache: loader_cache,
loader_group: loader_group,
path: exec_field.path,
Expand All @@ -199,17 +206,20 @@ def resolve_execution_field(exec_field, resolved_sources)
@exec_queue << ExecutionScope.new(
parent_type: return_type,
selections: exec_field.selections,
sources: next_sources,
responses: next_responses,
objects: next_objects,
results: next_results,
path: exec_field.path,
parent: exec_field.scope,
)
end
else
# build leaf results
resolved_sources.each_with_index do |val, i|
i = 0
while i < resolved_objects.length
# DANGER: HOT PATH!
parent_responses[i][field_key] = coerce_leaf_value(exec_field, field_type, val)
val = resolved_objects[i]
parent_results[i][field_key] = build_leaf_result(exec_field, field_type, val)
i += 1
end
end
end
Expand Down
10 changes: 5 additions & 5 deletions lib/graphql/cardinal/executor/execution_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
module GraphQL::Cardinal
class Executor
class ExecutionScope
attr_reader :parent_type, :selections, :sources, :responses, :path, :parent
attr_reader :parent_type, :selections, :objects, :results, :path, :parent
attr_accessor :fields

def initialize(
parent_type:,
selections:,
sources:,
responses:,
objects:,
results:,
loader_cache: nil,
loader_group: nil,
path: [],
parent: nil
)
@parent_type = parent_type
@selections = selections
@sources = sources
@responses = responses
@objects = objects
@results = results
@loader_cache = loader_cache
@loader_group = loader_group
@path = path.freeze
Expand Down
64 changes: 29 additions & 35 deletions lib/graphql/cardinal/executor/hot_paths.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,33 @@
module GraphQL::Cardinal
class Executor
module HotPaths
INCORRECT_LIST_VALUE = "Incorrect result for list field. Expected Array, got ".freeze

# DANGER: HOT PATH!
# DANGER: HOT PATHS!
# Overhead added here scales dramatically...
def build_composite_response(exec_field, current_type, source, next_sources, next_responses)
# if object authorization check implemented, then...
# unless Authorization.can_access_object?(return_type, source, @context)

if source.nil? || source.is_a?(ExecutionError)
build_missing_value(exec_field, current_type, source)
INCORRECT_LIST_VALUE = "Incorrect result for list field. Expected Array, got ".freeze

def build_composite_result(exec_field, current_type, object, next_objects, next_results)
if object.nil? || object.is_a?(ExecutionError)
build_missing_value(exec_field, current_type, object)
elsif current_type.list?
unless source.is_a?(Array)
report_exception("#{INCORRECT_LIST_VALUE}#{source.class}", field: exec_field)
unless object.is_a?(Array)
report_exception("#{INCORRECT_LIST_VALUE}#{object.class}", field: exec_field)
return build_missing_value(exec_field, current_type, nil)
end

current_type = current_type.of_type while current_type.non_null?

source.map do |src|
build_composite_response(exec_field, current_type.of_type, src, next_sources, next_responses)
object.map do |src|
build_composite_result(exec_field, current_type.of_type, src, next_objects, next_results)
end
else
next_sources << source
next_responses << ResponseHash.new
next_responses.last
end
end

# DANGER: HOT PATH!
# Overhead added here scales dramatically...
def build_missing_value(exec_field, current_type, val)
# the provided value should always be nil or an error object
if current_type.non_null?
val ||= InvalidNullError.new(path: exec_field.path)
end

if val
val.replace_path(exec_field.path) unless val.path
@errors << val unless val.base_error?
next_objects << object
next_results << ResultHash.new
next_results.last
end

val
end

# DANGER: HOT PATH!
# Overhead added here scales dramatically...
def coerce_leaf_value(exec_field, current_type, val)
def build_leaf_result(exec_field, current_type, val)
if val.nil? || val.is_a?(StandardError)
build_missing_value(exec_field, current_type, val)
elsif current_type.list?
Expand All @@ -60,7 +40,7 @@ def coerce_leaf_value(exec_field, current_type, val)

current_type = current_type.of_type while current_type.non_null?

val.map { coerce_leaf_value(exec_field, current_type.of_type, _1) }
val.map { build_leaf_result(exec_field, current_type.of_type, _1) }
else
begin
current_type.unwrap.coerce_result(val, @context)
Expand All @@ -71,6 +51,20 @@ def coerce_leaf_value(exec_field, current_type, val)
end
end
end

def build_missing_value(exec_field, current_type, val)
# the provided value should always be nil or an error object
if current_type.non_null?
val ||= InvalidNullError.new(path: exec_field.path)
end

if val
val.replace_path(exec_field.path) unless val.path
@errors << val unless val.base_error?
end

val
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module GraphQL::Cardinal
class Executor
class ResponseHash < Hash
class ResultHash < Hash
attr_accessor :typename
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/graphql/cardinal/field_resolvers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def resolve(objects, _args, _ctx, _scope)
raise NotImplementedError, "Resolver#resolve must be implemented."
end

def map_sources(objects)
def map_objects(objects)
objects.map do |obj|
yield(obj)
rescue StandardError => e
Expand All @@ -30,7 +30,7 @@ def initialize(key)
end

def resolve(objects, _args, _ctx, _scope)
map_sources(objects) do |hash|
map_objects(objects) do |hash|
hash[@key]
end
end
Expand All @@ -42,7 +42,7 @@ def initialize(name)
end

def resolve(objects, _args, _ctx, _scope)
map_sources(objects) do |obj|
map_objects(objects) do |obj|
obj.public_send(@name)
end
end
Expand All @@ -51,7 +51,7 @@ def resolve(objects, _args, _ctx, _scope)
class TypenameResolver < FieldResolver
def resolve(objects, _args, _ctx, scope)
typename = scope.parent_type.graphql_name.freeze
map_sources(objects) { typename }
map_objects(objects) { typename }
end
end
end
Expand Down
Loading