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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@

- Runtime: add hooks for experimental custom runtimes #5425, #5429
- Lazy handling and Dataloader have been merged under the hood #5422
- Doc: merk `load_application_object_failed` as public #5426
- Doc: mark `load_application_object_failed` as public #5426

# 2.5.11 (9 Jul 2025)

Expand Down
8 changes: 4 additions & 4 deletions guides/execution/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ Adopting a feature flag system (described below) can also make this easier.

When all tests pass on `.execute_next`, you're ready to try it out in production.

## COMING SOON: Migration and Clean-Up Script
## Migration and Clean-Up Script

Migrating field implementations can be automated in many cases. A script to analyze and execute these cases is in the works: [Pull Request](https://github.com/rmosolgo/graphql-ruby/pull/5531). This script will also be able to clean up unused instance methods when the migration is complete.
`graphql_migrate_execution` is a command-line development tool that can automate many common GraphQL-Ruby field resolver patterns. Check out its docs and try out: https://rmosolgo.github.io/graphql_migrate_execution/

## Production Considerations

Expand Down Expand Up @@ -159,7 +159,7 @@ Previously, GraphQL-Ruby would check `type_object.respond_to?(:title)`, `object.

Now, GraphQL-Ruby simply calls `object.title` and allows the `NoMethodError` to bubble up if one is raised.

### Query Analyzers, including complexity 🌕
### Query Analyzers, including complexity 🟡

Support is identical; this runs before execution using the exact same code.

Expand Down Expand Up @@ -219,7 +219,7 @@ Not supported yet. This will need some new kind of integration.

These methods/procs are called.

### `validates:`
### `validates:` 🟡

Built-in validators are supported. Custom validators will always receive `nil` as the `object`. (`object` is no longer available; this API will probably change before this is fully released.)

Expand Down
15 changes: 5 additions & 10 deletions lib/graphql/execution/next/field_resolve_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,19 @@ def append_selection(ast_node)
end

def coerce_arguments(argument_owner, ast_arguments_or_hash, run_loads = true)
arg_defns = argument_owner.arguments(@selections_step.query.context)
arg_defns = @selections_step.query.types.arguments(argument_owner)
if arg_defns.empty?
return EmptyObjects::EMPTY_HASH
end
args_hash = {}

if ast_arguments_or_hash.nil? # This can happen with `.trigger`
return args_hash
end

arg_inputs_are_h = ast_arguments_or_hash.is_a?(Hash)

arg_defns.each do |arg_graphql_name, arg_defn|
arg_defns.each do |arg_defn|
arg_value = nil
was_found = false
if arg_inputs_are_h
Expand Down Expand Up @@ -279,14 +280,6 @@ 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}")
if field_name == "__typename"
# TODO handle custom introspection
@field_results = Array.new(@selections_step.objects.size, @parent_type.graphql_name)
@object_is_authorized = AlwaysAuthorized
build_results
return
end

arguments = coerce_arguments(@field_definition, @ast_node.arguments) # rubocop:disable Development/ContextIsPassedCop
@arguments ||= arguments # may have already been set to an error

Expand Down Expand Up @@ -727,6 +720,8 @@ def resolve_batch(objects, context, args_hash)
obj_inst = @owner.wrap(obj_inst, context)
end
obj_inst.public_send(@field_definition.execution_next_mode_key, **args_hash)
rescue GraphQL::ExecutionError => exec_err
exec_err
end
else
raise "Batching execution for #{path} not implemented (execution_next_mode: #{@execution_next_mode.inspect}); provide `resolve_static:`, `resolve_batch:`, `hash_key:`, `method:`, or use a compatibility plug-in"
Expand Down
6 changes: 5 additions & 1 deletion lib/graphql/execution/next/load_argument_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ def value

def call
context = @field_resolve_step.selections_step.query.context
@loaded_value = @load_receiver.load_and_authorize_application_object(@argument_definition, @argument_value, context)
@loaded_value = begin
@load_receiver.load_and_authorize_application_object(@argument_definition, @argument_value, context)
rescue GraphQL::UnauthorizedError => auth_err
context.schema.unauthorized_object(auth_err)
end
if (runner = @field_resolve_step.runner).resolves_lazies && runner.lazy?(@loaded_value)
runner.dataloader.lazy_at_depth(@field_resolve_step.path.size, self)
else
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/execution/next/prepare_object_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def authorize
begin
query.current_trace.begin_authorized(@resolved_type, @object, query.context)
@authorized_value = @resolved_type.authorized?(@object, query.context)
query.current_trace.end_authorized(@resolve_type, @object, query.context, @authorized_value)
query.current_trace.end_authorized(@resolved_type, @object, query.context, @authorized_value)
rescue GraphQL::UnauthorizedError => auth_err
@authorization_error = auth_err
end
Expand All @@ -83,7 +83,7 @@ def authorize
else
create_result
end
rescue GraphQL::Error => err
rescue GraphQL::RuntimeError => err
@graphql_result[@key] = @field_resolve_step.add_graphql_error(err)
end

Expand Down
7 changes: 4 additions & 3 deletions lib/graphql/execution/next/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ def initialize(multiplex, authorization:)
end

def resolve_type(type, object, query)
query.current_trace.begin_resolve_type(@static_type, object, query.context)
query.current_trace.begin_resolve_type(type, object, query.context)
resolved_type, _ignored_new_value = query.resolve_type(type, object)
query.current_trace.end_resolve_type(@static_type, object, query.context, resolved_type)
query.current_trace.end_resolve_type(type, object, query.context, resolved_type)
resolved_type
end

Expand Down Expand Up @@ -199,7 +199,8 @@ def execute
res_h
end

GraphQL::Query::Result.new(query: query, values: fin_result)
query.result_values = fin_result
query.result
end
end
ensure
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/execution_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module GraphQL
class ExecutionError < GraphQL::RuntimeError
# @return [GraphQL::Language::Nodes::Field] the field where the error occurred
def ast_node
ast_nodes.first
ast_nodes&.first
end

def ast_node=(new_node)
Expand Down
6 changes: 5 additions & 1 deletion lib/graphql/schema/member/has_fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,14 @@ def field_class(new_field_class = nil)

def global_id_field(field_name, **kwargs)
type = self
field field_name, "ID", **kwargs, null: false
field field_name, "ID", **kwargs, null: false, resolve_each: true
define_method(field_name) do
context.schema.id_from_object(object, type, context)
end

define_singleton_method(field_name) do |object, context|
context.schema.id_from_object(object, type, context)
end
end

# @param new_has_no_fields [Boolean] Call with `true` to make this Object type ignore the requirement to have any defined fields.
Expand Down
7 changes: 6 additions & 1 deletion lib/graphql/schema/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ def call
q = context.query
trace_objs = [object]
q.current_trace.begin_execute_field(field, @prepared_arguments, trace_objs, q)
is_authed, new_return_value = authorized?(**@prepared_arguments)
begin
is_authed, new_return_value = authorized?(**@prepared_arguments)
rescue GraphQL::UnauthorizedError => err
new_return_value = q.schema.unauthorized_object(err)
is_authed = true # the error was handled
end

if (runner = @field_resolve_step.runner).resolves_lazies && runner.schema.lazy?(is_authed)
is_authed, new_return_value = runner.schema.sync_lazy(is_authed)
Expand Down