Skip to content
Open
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
73 changes: 66 additions & 7 deletions lib/graphql/execution/field_resolve_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def initialize(parent_type:, runner:, key:, selections_step:)
@results = nil
@finish_extension_idx = nil
@was_scoped = nil
@field_results_are_eager = nil
@pending_steps = nil
@arguments_without_loads = @post_processors = @directive_finalizers = nil
end
Expand Down Expand Up @@ -365,8 +366,10 @@ def execute_field
query.current_trace.end_execute_field(@field_definition, authorized_objects, @arguments, query, @field_results)

if any_lazy_results?
@field_results_are_eager = false
@runner.dataloader.lazy_at_depth(path.size, self)
elsif @pending_steps.nil? || @pending_steps.empty?
@field_results_are_eager = true if @field_results_are_eager.nil?
if has_extensions
finish_extensions
else
Expand Down Expand Up @@ -439,6 +442,7 @@ def finish_extensions
end
@finish_extension_idx += 1
if any_lazy_results?
@field_results_are_eager = false
@runner.dataloader.lazy_at_depth(path.size, self)
return
end
Expand Down Expand Up @@ -472,13 +476,24 @@ def build_results

is_list = return_type.list?
is_non_null = return_type.non_null?
i = 0
s = @results.size
while i < s do
result_h = @results[i]
result = @field_results[i]
i += 1
build_graphql_result(result_h, @key, result, return_type, is_non_null, is_list, false)
if skip_prepare_object_steps?(return_type)
i = 0
s = @results.size
while i < s do
result_h = @results[i]
result = @field_results[i]
i += 1
build_graphql_result_without_prepare_object_step(result_h, @key, result, return_type, is_non_null, is_list, false)
end
else
i = 0
s = @results.size
while i < s do
result_h = @results[i]
result = @field_results[i]
i += 1
build_graphql_result(result_h, @key, result, return_type, is_non_null, is_list, false)
end
end
@enqueued_authorization = true

Expand Down Expand Up @@ -683,6 +698,50 @@ def build_graphql_result(graphql_result, key, field_result, return_type, is_nn,
end
end

def build_graphql_result_without_prepare_object_step(graphql_result, key, field_result, return_type, is_nn, is_list, is_from_array) # rubocop:disable Metrics/ParameterLists
if field_result.nil? || field_result.is_a?(Finalizer)
build_graphql_result(graphql_result, key, field_result, return_type, is_nn, is_list, is_from_array)
elsif is_list
if is_nn
return_type = return_type.of_type
end
inner_type = return_type.of_type
inner_type_nn = inner_type.non_null?
inner_type_l = inner_type.list?
list_result = graphql_result[key] = []
i = 0
s = field_result.size
while i < s
inner_f_r = field_result[i]
build_graphql_result_without_prepare_object_step(list_result, i, inner_f_r, inner_type, inner_type_nn, inner_type_l, true)
i += 1
end
else
if @runner.resolves_lazies
query = @selections_step.query
query.current_trace.begin_authorized(@static_type, field_result, query.context)
query.current_trace.end_authorized(@static_type, field_result, query.context, true)
end
next_result_h = {}.compare_by_identity
@all_next_results << next_result_h
@all_next_objects << field_result
@runner.static_type_at[next_result_h] = @static_type
graphql_result[key] = next_result_h
end
end

def skip_prepare_object_steps?(return_type)
return false unless @field_results_are_eager &&
!@was_scoped &&
@post_processors.nil? &&
@directive_finalizers.nil? &&
@static_type.kind.object? &&
!@runner.authorizes?(@static_type, @selections_step.query.context)

outer_type = return_type.non_null? ? return_type.of_type : return_type
!outer_type.list? || !outer_type.of_type.list?
end

def resolve_batch(objects, context, args_hash)
dyn_ins = @field_definition.dynamic_introspection
method_receiver = dyn_ins ? @field_definition.owner : @parent_type
Expand Down
20 changes: 15 additions & 5 deletions lib/graphql/execution/input_values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
module GraphQL
module Execution
class InputValues
ARGUMENT_NODES_INDEX_THRESHOLD = 32

def initialize(query, runner)
@query = query
@runner = runner
Expand Down Expand Up @@ -39,11 +41,20 @@ def argument_values(owner_defn, argument_nodes, field_resolve_step)
arg_defns = @query.types.arguments(owner_defn)
argument_values = {}
errors = nil
argument_nodes_by_name = if argument_nodes.length >= ARGUMENT_NODES_INDEX_THRESHOLD
argument_nodes.each_with_object({}) do |arg_node, index|
index[arg_node.name] ||= arg_node
end
end

arg_defns.each do |argument_definition|
arg_ruby_key = argument_definition.keyword
arg_graphql_key = argument_definition.graphql_name
arg_node = argument_nodes.find { |a| a.name == arg_graphql_key }
arg_node = if argument_nodes_by_name
argument_nodes_by_name[arg_graphql_key]
else
argument_nodes.find { |a| a.name == arg_graphql_key }
end
if arg_node.nil? || (arg_node.value.is_a?(Language::Nodes::VariableIdentifier) && !variable_values.key?(arg_node.value.name))
if argument_definition.default_value?
arg_value = value_from_ast(argument_definition.default_value, argument_definition.type)
Expand Down Expand Up @@ -198,14 +209,13 @@ def argument_value(argument_values, argument_key, argument_definition, arg_value
if argument_definition.type.list?
results = Array.new(arg_value.size, nil)
argument_values[argument_key] = results
arg_value.each_with_index do |inner_v, idx|
loads_step = LoadArgumentStep.new(
if !arg_value.empty?
loads_step = LoadArgumentsStep.new(
field_resolve_step: field_resolve_step,
load_receiver: load_receiver,
argument_value: inner_v,
argument_values: arg_value,
argument_definition: argument_definition,
arguments: results,
argument_key: idx,
)
ps.push(loads_step)
@runner.add_step(loads_step)
Expand Down
184 changes: 184 additions & 0 deletions lib/graphql/execution/load_arguments_step.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# frozen_string_literal: true
module GraphQL
module Execution
class LoadArgumentsStep
def initialize(field_resolve_step:, arguments:, load_receiver:, argument_values:, argument_definition:)
@field_resolve_step = field_resolve_step
@load_receiver = load_receiver
@arguments = arguments
@argument_values = argument_values
@argument_definition = argument_definition
@loaded_values = Array.new(argument_values.size)
@authorization_states = Array.new(argument_values.size, true)
@errors = Array.new(argument_values.size)
@remaining_values = argument_values.size
@remaining_loads = argument_values.size
@phase = :start
@next_index = 0
@lazy_indexes = []
end

def call
case @phase
when :start
@phase = :loading
enqueue_jobs(@argument_values.size)
when :loading
index = @next_index
@next_index += 1
load_value(index)
when :resolving
index = @lazy_indexes[@next_index]
@next_index += 1
resolve_lazy_value(index)
else
raise GraphQL::InvariantError, "Unexpected LoadArgumentsStep phase: #{@phase.inspect}"
end
nil
end

def value
@phase = :resolving
@next_index = 0
enqueue_jobs(@lazy_indexes.size)
nil
end

private

def load_value(index)
@field_resolve_step.set_current_field
context = @field_resolve_step.selections_step.query.context
begin
@loaded_values[index] = begin
@load_receiver.load_and_authorize_application_object(
@argument_definition,
@argument_values[index],
context,
)
rescue GraphQL::UnauthorizedError => auth_err
@authorization_states[index] = false
context.schema.unauthorized_object(auth_err)
end
complete_load(index)
rescue GraphQL::RuntimeError => err
handle_runtime_error(index, err, loading: true)
complete_load(index)
rescue StandardError => stderr
handle_standard_error(index, stderr)
complete_load(index)
ensure
@field_resolve_step.set_current_field(nil)
end
end

def resolve_lazy_value(index)
@field_resolve_step.set_current_field
schema = @field_resolve_step.runner.schema
begin
@loaded_values[index] = schema.sync_lazy(@loaded_values[index])
complete_value(index)
rescue GraphQL::UnauthorizedError => auth_err
@authorization_states[index] = false
schema.unauthorized_object(auth_err)
rescue GraphQL::RuntimeError => err
handle_runtime_error(index, err, loading: false)
complete_value(index)
rescue StandardError => stderr
handle_standard_error(index, stderr)
complete_value(index)
ensure
@field_resolve_step.set_current_field(nil)
end
end

def handle_runtime_error(index, error, loading:)
if error.is_a?(Schema::Subscription::EarlyUnsubscribe)
@authorization_states[index] = false if loading
@loaded_values[index] = error.unsubscribed_result
else
@loaded_values[index] = @errors[index] = error
end
end

def handle_standard_error(index, error)
query = @field_resolve_step.selections_step.query
@loaded_values[index] = begin
query.handle_or_reraise(
error,
field: @field_resolve_step.field_definition,
arguments: @field_resolve_step.arguments, # rubocop:disable Development/ContextIsPassedCop
object: nil,
)
rescue GraphQL::ExecutionError => execution_error
execution_error
end
end

def record_error(index)
loaded_value = @loaded_values[index]
@errors[index] = loaded_value if loaded_value.is_a?(GraphQL::RuntimeError)
end

def complete_load(index)
record_error(index)
load_completed(index)
end

def complete_value(index)
record_error(index)
value_completed(index)
end

def enqueue_jobs(count)
dataloader = @field_resolve_step.runner.dataloader
count.times { dataloader.append_job(self) }
end

def load_completed(index)
runner = @field_resolve_step.runner
if runner.resolves_lazies && runner.lazy?(@loaded_values[index])
@lazy_indexes << index
else
value_completed(index)
end

@remaining_loads -= 1
if @remaining_loads == 0 && !@lazy_indexes.empty?
@phase = :waiting
runner.dataloader.lazy_at_depth(@field_resolve_step.path.size, self)
end
end

def value_completed(index)
query = @field_resolve_step.selections_step.query
if (error = @errors[index])
error.path = @field_resolve_step.path
@field_resolve_step.arguments = error
elsif @authorization_states[index]
loaded_value = @loaded_values[index]
query.current_trace.object_loaded(@argument_definition, loaded_value, query.context)
@arguments[index] = loaded_value
else
@field_resolve_step.arguments = EmptyObjects::EMPTY_HASH
@field_resolve_step.pending_steps.clear
@field_resolve_step.build_errors_result(nil, nil)
end

@remaining_values -= 1
return if @remaining_values > 0 || (!@authorization_states[index] && error.nil?)

finish
end

def finish
@phase = :finished
field_pending_steps = @field_resolve_step.pending_steps
field_pending_steps.delete(self)
if @field_resolve_step.arguments && field_pending_steps.empty? # rubocop:disable Development/ContextIsPassedCop
@field_resolve_step.runner.add_step(@field_resolve_step)
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/graphql/execution/next.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "graphql/execution/field_resolve_step"
require "graphql/execution/finalize"
require "graphql/execution/load_argument_step"
require "graphql/execution/load_arguments_step"
require "graphql/execution/resolve_type_step"
require "graphql/execution/runner"
require "graphql/execution/selections_step"
Expand Down
19 changes: 19 additions & 0 deletions spec/graphql/execution/input_values_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ class Mutation < GraphQL::Schema::Object
field :test_list_input, Boolean do
argument :input, [TestInput, null: true], required: false
end

field :test_many_arguments, Boolean do
8.times do |i|
argument :"arg#{i}", String, required: false
end
end
end

mutation(Mutation)
Expand Down Expand Up @@ -74,6 +80,19 @@ def test_it_produces_argument_values_for_input_objects
assert_equal_input( {input: { string: "a", enum: "ACTIVE" } }, input.argument_values(TestSchema.find("Mutation.testInput"), get_argument_nodes("input: { string: \"a\", enum: ACTIVE }"), nil))
end

def test_it_keeps_the_first_duplicate_argument
input = get_input_values
argument_strings = [
'arg0: "first"',
'arg0: "second"',
] + 30.times.map { |i| %(arg#{(i % 7) + 1}: "value") }
argument_nodes = get_argument_nodes(argument_strings.join(", "))
argument_values, errors = input.argument_values(TestSchema.find("Mutation.testManyArguments"), argument_nodes, nil)

assert_nil errors
assert_equal "first", argument_values[:arg0]
end

def assert_equal_input(expected_ruby_hash, graphql_input, path = [])
if path.empty? && graphql_input.is_a?(Array) && graphql_input.last.nil? && expected_ruby_hash.is_a?(Hash)
graphql_input = graphql_input.first # ignore the `nil` errors in the multiple return
Expand Down
Loading
Loading