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
3 changes: 3 additions & 0 deletions guides/_plugins/api_doc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,12 @@ def render(context)
end
text = h.gsub(/^#+ /, "")
target = text.downcase
.gsub("🟡", "00emoji00")
.gsub("❌", "00emoji00")
.gsub(/[^a-z0-9_]+/, "-")
.sub(/-$/, "")
.sub(/^-/, "")
.gsub("-00emoji00", "-")

rendered_text = Kramdown::Document.new(text, auto_ids: false)
.to_html
Expand Down
25 changes: 20 additions & 5 deletions guides/execution/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,23 @@ This is not supported because the new runtime doesn't actually produce `current_

It is theoretically possible to support this but it will be a ton of work. If you use this for core runtime functions, please share your use case in a GitHub issue and we can investigate future options.

### Scoped context ❌

This is currently implemented with `current_path`. Another implementation is probably possible but not implemented yet. Please open an issue to discuss.

### `@defer` and `@stream` ❌

This depends on `current_path` so isn't possible yet.
`@defer` is supported with an implementation difference that _probably_ doesn't affect your application: previously, `@defer` worked by pausing and resuming the _same `GraphQL::Query` instance_. However, with `Execution::Next`, `@defer` takes a different approach. Instead, when a `GraphQL::Query` encounters `@defer`, it notes the location in the document and stops executing that branch. Later, when you request the deferred result, that branch of the query is resumed using a new instance of `GraphQL::Query::Partial`.

This might matter if you're modifying `context` at runtime because those new instances _also_ have fresh `Query::Context` instances. The original query context _will_ get copied into the `@defer` branches using `Query::Context.new(**original_query.context.to_h)`, so any custom values will be available. But if you _assign new keys_ after the context is copied, those keys won't appear when running later `@defer`ed branches.

To handle this, you can refactor how you accumulate data during execution. Instead of `||=`'ing into `context[...]` during execution, assign a new accumulator object _before_ starting the query, then call methods on that object to make any necessary state changes. That new object _will_ be copied into `@defer` partials, and since the object is shared between the different branches, any necessary state changes will still be "seen" everywhere.

If this gives you trouble, please feel free to email me or open an issue on GitHub to discuss a migration strategy.

##### GraphQL-Batch support

When using `Execution::Next`, no custom code is required to support `graphql-batch`.

### ObjectCache ❌

Expand Down Expand Up @@ -242,12 +256,13 @@ Resolver classes are called.

### `raw_value` 🟡

Supported but requires a manual opt-in at schema level. Support for this will probably get better somehow in a future version.
Supported, but the `raw_value` call must be made on `context`, for example:

```ruby
class MyAppSchema < GraphQL::Schema
uses_raw_value(true) # TODO This configuration will be improved in a future GraphQL-Ruby version
use GraphQL::Execution::Next
field :values, SomeObjectType, resolve_static: true

def self.values(context)
context.raw_value(...)
end
```

Expand Down
4 changes: 1 addition & 3 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ def self.eager_load!
class Error < StandardError
end

class RuntimeError < Error
end

# This error is raised when GraphQL-Ruby encounters a situation
# that it *thought* would never happen. Please report this bug!
class InvariantError < Error
Expand Down Expand Up @@ -122,6 +119,7 @@ class << self
autoload :ParseError, "graphql/parse_error"
autoload :Backtrace, "graphql/backtrace"

autoload :RuntimeError, "graphql/runtime_error"
autoload :UnauthorizedError, "graphql/unauthorized_error"
autoload :UnauthorizedEnumValueError, "graphql/unauthorized_enum_value_error"
autoload :UnauthorizedFieldError, "graphql/unauthorized_field_error"
Expand Down
33 changes: 20 additions & 13 deletions lib/graphql/analysis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ def analyze_multiplex(multiplex, analyzers)
end
end

multiplex_results = multiplex_analyzers.map(&:result)
multiplex_errors = analysis_errors(multiplex_results)

multiplex_analyzers.map!(&:result)
multiplex_errors = analysis_errors(EmptyObjects::EMPTY_ARRAY, multiplex_analyzers)
multiplex.queries.each_with_index do |query, idx|
query.analysis_errors = multiplex_errors + analysis_errors(query_results[idx])
query.analysis_errors = analysis_errors(multiplex_errors, query_results[idx])
end
multiplex_results
multiplex_analyzers
end
end

Expand All @@ -55,13 +55,11 @@ def analyze_multiplex(multiplex, analyzers)
# @return [Array<Any>] Results from those analyzers
def analyze_query(query, analyzers, multiplex_analyzers: [])
query.current_trace.analyze_query(query: query) do
query_analyzers = analyzers
.map { |analyzer| analyzer.new(query) }
.tap { _1.select!(&:analyze?) }

query_analyzers = analyzers.map { |analyzer| analyzer.new(query) }
query_analyzers.select!(&:analyze?)
analyzers_to_run = query_analyzers + multiplex_analyzers
if !analyzers_to_run.empty?

if !analyzers_to_run.empty?
analyzers_to_run.select!(&:visit?)
if !analyzers_to_run.empty?
visitor = GraphQL::Analysis::Visitor.new(
Expand All @@ -79,18 +77,27 @@ def analyze_query(query, analyzers, multiplex_analyzers: [])

query_analyzers.map(&:result)
else
[]
EmptyObjects::EMPTY_ARRAY
end
end
rescue TimeoutError => err
[err]
rescue GraphQL::UnauthorizedError, GraphQL::ExecutionError
# This error was raised during analysis and will be returned the client before execution
[]
EmptyObjects::EMPTY_ARRAY
end

def analysis_errors(results)
results.flatten.tap { _1.select! { |r| r.is_a?(GraphQL::AnalysisError) } }
def analysis_errors(parent_errors, results)
if !results.empty?
results = results.flatten
results.select! { |r| r.is_a?(GraphQL::AnalysisError) }
end

if parent_errors.empty?
results
else
parent_errors + results
end
end
end
end
4 changes: 2 additions & 2 deletions lib/graphql/execution.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# frozen_string_literal: true
require "graphql/execution/directive_checks"
require "graphql/execution/next"
require "graphql/execution/interpreter"
require "graphql/execution/lazy"
require "graphql/execution/lookahead"
require "graphql/execution/multiplex"
require "graphql/execution/next"
require "graphql/execution/errors"

module GraphQL
Expand All @@ -14,7 +14,7 @@ class Skip < GraphQL::RuntimeError
attr_accessor :path
def ast_nodes=(_ignored); end

def assign_graphql_result(query, result_data, key)
def finalize_graphql_result(query, result_data, key)
result_data.delete(key)
end
end
Expand Down
6 changes: 6 additions & 0 deletions lib/graphql/execution/interpreter/handles_raw_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ module Execution
class Interpreter
# Wrapper for raw values
class RawValue
include GraphQL::Execution::Next::Finalizer

def finalize_graphql_result(query, result_data, result_key)
result_data[result_key] = @object
end

def initialize(obj = nil)
@object = obj
end
Expand Down
11 changes: 11 additions & 0 deletions lib/graphql/execution/next.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ def self.run_all(schema, query_options, context: {}, max_complexity: schema.max_
runner = Runner.new(multiplex, **schema.execution_next_options)
runner.execute
end

module Finalizer
attr_accessor :path
def finalize_graphql_result(query, result_data, result_key)
raise RequiredImplementationMissingError
end

def continue_execution?
true
end
end
end
end
end
111 changes: 76 additions & 35 deletions lib/graphql/execution/next/field_resolve_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def coerce_argument_value(arguments, arg_defn, arg_value, run_loads, target_keyw
end
end

if arg_value.is_a?(GraphQL::Error)
if arg_value.is_a?(GraphQL::RuntimeError)
@arguments = arg_value
elsif run_loads && arg_defn.loads && as_type.nil? && !arg_value.nil?
# This is for legacy compat:
Expand Down Expand Up @@ -279,7 +279,7 @@ def self.[](_key)
def build_arguments
query = @selections_step.query
field_name = @ast_node.name
@field_definition = query.get_field(@parent_type, field_name) || raise("Invariant: no field found for #{@parent_type.to_type_signature}.#{ast_node.name}")
@field_definition = query.types.field(@parent_type, field_name) || raise("Invariant: no field found for #{@parent_type.to_type_signature}.#{ast_node.name}")
arguments = coerce_arguments(@field_definition, @ast_node.arguments) # rubocop:disable Development/ContextIsPassedCop
@arguments ||= arguments # may have already been set to an error

Expand All @@ -292,7 +292,7 @@ def build_arguments
def execute_field
objects = @selections_step.objects
# TODO not as good because only one error?
if @arguments.is_a?(GraphQL::Error)
if @arguments.is_a?(GraphQL::RuntimeError)
@field_results = Array.new(objects.size, @arguments)
@object_is_authorized = AlwaysAuthorized
build_results
Expand Down Expand Up @@ -368,6 +368,43 @@ def execute_field
end

query.current_trace.begin_execute_field(@field_definition, @arguments, authorized_objects, query)

if @runner.uses_runtime_directives
if @ast_nodes.nil? || @ast_nodes.size == 1
directives = if @ast_node.directives.any?
@ast_node.directives
else
nil
end
else
directives = nil
@ast_nodes.each do |n|
if (d = n.directives).any? # rubocop:disable Development/NoneWithoutBlockCop
directives ||= []
directives.concat(d)
end
end
end

if directives
directives.each do |dir_node|
if (dir_defn = @runner.runtime_directives[dir_node.name])
# TODO: `coerce_arguments` modifies self, assuming it's field arguments. Extract to pure function for use
# here and with fragments.
dir_args = coerce_arguments(dir_defn, dir_node.arguments, false)
result = dir_defn.resolve_field(ast_nodes, @parent_type, field_definition, authorized_objects, dir_args, ctx)
if result.is_a?(Finalizer)
result.path = path
query.add_finalizer(result, true)
if !result.continue_execution?
return
end
end
end
end
end
end

has_extensions = @field_definition.extensions.size > 0
if has_extensions
@extended = GraphQL::Schema::Field::ExtendedState.new(@arguments, authorized_objects)
Expand Down Expand Up @@ -437,7 +474,7 @@ def finish_extensions
if @was_scoped.nil?
if (rt = @field_definition.type.unwrap).respond_to?(:scope_items)
@was_scoped = true
@field_results = @field_results.map { |v| v.nil? ? v : rt.scope_items(v, ctx) }
@field_results.map! { |v| v.nil? ? v : rt.scope_items(v, ctx) }
else
@was_scoped = false
end
Expand Down Expand Up @@ -520,8 +557,13 @@ def build_results
else
nil
end
elsif field_result.is_a?(GraphQL::Error)
add_graphql_error(field_result)
elsif field_result.is_a?(Finalizer)
if field_result.is_a?(GraphQL::RuntimeError)
add_graphql_error(field_result)
else
field_result.path = path
ctx.query.add_finalizer(finalizer)
end
else
# TODO `nil`s in [T!] types aren't handled
return_type.coerce_result(field_result, ctx)
Expand Down Expand Up @@ -596,8 +638,14 @@ def build_graphql_result(graphql_result, key, field_result, return_type, is_nn,
else
graphql_result[key] = nil
end
elsif field_result.is_a?(GraphQL::Error)
graphql_result[key] = add_graphql_error(field_result)
elsif field_result.is_a?(Finalizer)
graphql_result[key] = if field_result.is_a?(GraphQL::RuntimeError)
add_graphql_error(field_result)
else
field_result.path = path
@selections_step.query.add_finalizer(field_result)
field_result
end
elsif is_list
if is_nn
return_type = return_type.of_type
Expand Down Expand Up @@ -633,10 +681,11 @@ def build_graphql_result(graphql_result, key, field_result, return_type, is_nn,
ps << obj_step
@runner.add_step(obj_step)
else
next_result_h = {}
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
st = @runner.static_type_at
st[next_result_h] = @static_type
graphql_result[key] = next_result_h
end
end
Expand All @@ -651,7 +700,11 @@ def resolve_batch(objects, context, args_hash)
Array.new(objects.size, exec_err)
end
when :resolve_static
result = method_receiver.public_send(@field_definition.execution_next_mode_key, context, **args_hash)
result = begin
method_receiver.public_send(@field_definition.execution_next_mode_key, context, **args_hash)
rescue GraphQL::ExecutionError => err
err
end
Array.new(objects.size, result)
when :resolve_each
objects.map do |o|
Expand All @@ -660,22 +713,20 @@ def resolve_batch(objects, context, args_hash)
err
end
when :hash_key
objects.map { |o| o[@field_definition.execution_next_mode_key] }
k = @field_definition.execution_next_mode_key
objects.map { |o| o[k] }
when :direct_send
if args_hash.empty?
objects.map do |o|
o.public_send(@field_definition.execution_next_mode_key)
rescue GraphQL::ExecutionError => err
err
rescue StandardError => stderr
begin
@selections_step.query.handle_or_reraise(stderr)
rescue GraphQL::ExecutionError => ex_err
ex_err
end
m = @field_definition.execution_next_mode_key
objects.map do |o|
o.public_send(m, **args_hash)
rescue GraphQL::ExecutionError => err
err
rescue StandardError => stderr
begin
@selections_step.query.handle_or_reraise(stderr)
rescue GraphQL::ExecutionError => ex_err
ex_err
end
else
objects.map { |o| o.public_send(@field_definition.execution_next_mode_key, **args_hash) }
end
when :dig
objects.map { |o| o.dig(*@field_definition.execution_next_mode_key) }
Expand Down Expand Up @@ -728,16 +779,6 @@ def resolve_batch(objects, context, args_hash)
end
end
end

class RawValueFieldResolveStep < FieldResolveStep
def build_graphql_result(graphql_result, key, field_result, return_type, is_nn, is_list, is_from_array) # rubocop:disable Metrics/ParameterLists
if field_result.is_a?(Interpreter::RawValue)
graphql_result[key] = field_result.resolve
else
super
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/graphql/execution/next/load_argument_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def call
private

def assign_value
if @loaded_value.is_a?(GraphQL::Error)
if @loaded_value.is_a?(GraphQL::RuntimeError)
@loaded_value.path = @field_resolve_step.path
@field_resolve_step.arguments = @loaded_value
else
Expand Down
Loading
Loading