diff --git a/CHANGELOG.md b/CHANGELOG.md index d50b79b04b..69aea345e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/guides/execution/migration.md b/guides/execution/migration.md index 997fb61787..498d365201 100644 --- a/guides/execution/migration.md +++ b/guides/execution/migration.md @@ -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 @@ -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. @@ -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.) diff --git a/lib/graphql/execution/next/field_resolve_step.rb b/lib/graphql/execution/next/field_resolve_step.rb index 0d484d4b8e..adcd5a5b74 100644 --- a/lib/graphql/execution/next/field_resolve_step.rb +++ b/lib/graphql/execution/next/field_resolve_step.rb @@ -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 @@ -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 @@ -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" diff --git a/lib/graphql/execution/next/load_argument_step.rb b/lib/graphql/execution/next/load_argument_step.rb index 7bae5f2f38..86d062d3ce 100644 --- a/lib/graphql/execution/next/load_argument_step.rb +++ b/lib/graphql/execution/next/load_argument_step.rb @@ -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 diff --git a/lib/graphql/execution/next/prepare_object_step.rb b/lib/graphql/execution/next/prepare_object_step.rb index 528f81402e..5898e91d42 100644 --- a/lib/graphql/execution/next/prepare_object_step.rb +++ b/lib/graphql/execution/next/prepare_object_step.rb @@ -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 @@ -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 diff --git a/lib/graphql/execution/next/runner.rb b/lib/graphql/execution/next/runner.rb index d3f96b67c1..24748842e1 100644 --- a/lib/graphql/execution/next/runner.rb +++ b/lib/graphql/execution/next/runner.rb @@ -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 @@ -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 diff --git a/lib/graphql/execution_error.rb b/lib/graphql/execution_error.rb index e3534c9c40..7b26dee5fd 100644 --- a/lib/graphql/execution_error.rb +++ b/lib/graphql/execution_error.rb @@ -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) diff --git a/lib/graphql/schema/member/has_fields.rb b/lib/graphql/schema/member/has_fields.rb index a45e959115..6e328a70e6 100644 --- a/lib/graphql/schema/member/has_fields.rb +++ b/lib/graphql/schema/member/has_fields.rb @@ -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. diff --git a/lib/graphql/schema/resolver.rb b/lib/graphql/schema/resolver.rb index 084290313e..b5312d1fec 100644 --- a/lib/graphql/schema/resolver.rb +++ b/lib/graphql/schema/resolver.rb @@ -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)