From 60af348a2184203b6706e6a1deb28c005eb5c4f7 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Tue, 26 Aug 2025 08:04:48 -0400 Subject: [PATCH] library updates. --- lib/graphql/cardinal/executor.rb | 94 ++++++++++--------- .../cardinal/executor/execution_scope.rb | 10 +- lib/graphql/cardinal/executor/hot_paths.rb | 64 ++++++------- .../{response_hash.rb => result_hash.rb} | 2 +- lib/graphql/cardinal/field_resolvers.rb | 8 +- lib/graphql/cardinal/introspection.rb | 36 +++---- lib/graphql/cardinal/tracer.rb | 6 +- 7 files changed, 112 insertions(+), 108 deletions(-) rename lib/graphql/cardinal/executor/{response_hash.rb => result_hash.rb} (80%) diff --git a/lib/graphql/cardinal/executor.rb b/lib/graphql/cardinal/executor.rb index cb828cc..6613169 100644 --- a/lib/graphql/cardinal/executor.rb +++ b/lib/graphql/cardinal/executor.rb @@ -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 @@ -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" @@ -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 @@ -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 @@ -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 @@ -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 @@ -121,8 +121,8 @@ 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 @@ -130,32 +130,35 @@ def execute_scope(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? @@ -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, @@ -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 diff --git a/lib/graphql/cardinal/executor/execution_scope.rb b/lib/graphql/cardinal/executor/execution_scope.rb index 1fb170b..ce2fb3e 100644 --- a/lib/graphql/cardinal/executor/execution_scope.rb +++ b/lib/graphql/cardinal/executor/execution_scope.rb @@ -3,14 +3,14 @@ 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: [], @@ -18,8 +18,8 @@ def initialize( ) @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 diff --git a/lib/graphql/cardinal/executor/hot_paths.rb b/lib/graphql/cardinal/executor/hot_paths.rb index c4f066d..9d975b2 100644 --- a/lib/graphql/cardinal/executor/hot_paths.rb +++ b/lib/graphql/cardinal/executor/hot_paths.rb @@ -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? @@ -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) @@ -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 diff --git a/lib/graphql/cardinal/executor/response_hash.rb b/lib/graphql/cardinal/executor/result_hash.rb similarity index 80% rename from lib/graphql/cardinal/executor/response_hash.rb rename to lib/graphql/cardinal/executor/result_hash.rb index 530f264..52a9577 100644 --- a/lib/graphql/cardinal/executor/response_hash.rb +++ b/lib/graphql/cardinal/executor/result_hash.rb @@ -2,7 +2,7 @@ module GraphQL::Cardinal class Executor - class ResponseHash < Hash + class ResultHash < Hash attr_accessor :typename end end diff --git a/lib/graphql/cardinal/field_resolvers.rb b/lib/graphql/cardinal/field_resolvers.rb index 5c49831..a5f76b5 100644 --- a/lib/graphql/cardinal/field_resolvers.rb +++ b/lib/graphql/cardinal/field_resolvers.rb @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/graphql/cardinal/introspection.rb b/lib/graphql/cardinal/introspection.rb index 19309cf..83d6f05 100644 --- a/lib/graphql/cardinal/introspection.rb +++ b/lib/graphql/cardinal/introspection.rb @@ -12,14 +12,14 @@ def resolve(objects, _args, ctx, _exec_field) class TypesResolver < FieldResolver def resolve(objects, _args, ctx, _exec_field) - types = ctx.query.types.all_types + types = ctx.types.all_types Array.new(objects.length, types) end end class DirectivesResolver < FieldResolver def resolve(objects, _args, ctx, _exec_field) - directives = ctx.query.types.directives + directives = ctx.types.directives Array.new(objects.length, directives) end end @@ -35,7 +35,7 @@ def resolve(objects, args, ctx, _exec_field) class TypeKindResolver < FieldResolver def resolve(objects, _args, ctx, _exec_field) - map_sources(objects) do |type| + map_objects(objects) do |type| type.kind.name end end @@ -43,9 +43,9 @@ def resolve(objects, _args, ctx, _exec_field) class EnumValuesResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |type| + map_objects(objects) do |type| if type.kind.enum? - enum_values = ctx.query.types.enum_values(type) + enum_values = ctx.types.enum_values(type) enum_values = enum_values.reject(&:deprecation_reason) unless args["includeDeprecated"] enum_values end @@ -55,9 +55,9 @@ def resolve(objects, args, ctx, _exec_field) class FieldsResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |type| + map_objects(objects) do |type| if type.kind.fields? - fields = ctx.query.types.fields(type) + fields = ctx.types.fields(type) fields = fields.reject(&:deprecation_reason) unless args["includeDeprecated"] fields end @@ -67,9 +67,9 @@ def resolve(objects, args, ctx, _exec_field) class InputFieldsResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |type| + map_objects(objects) do |type| if type.kind.input_object? - fields = ctx.query.types.arguments(type) + fields = ctx.types.arguments(type) fields = fields.reject(&:deprecation_reason) unless args["includeDeprecated"] fields end @@ -79,15 +79,15 @@ def resolve(objects, args, ctx, _exec_field) class InterfacesResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |type| - ctx.query.types.interfaces(type) if type.kind.fields? + map_objects(objects) do |type| + ctx.types.interfaces(type) if type.kind.fields? end end end class PossibleTypesResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |type| + map_objects(objects) do |type| ctx.query.possible_types(type) if type.kind.abstract? end end @@ -95,7 +95,7 @@ def resolve(objects, args, ctx, _exec_field) class OfTypeResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |type| + map_objects(objects) do |type| type.of_type if type.kind.wraps? end end @@ -103,7 +103,7 @@ def resolve(objects, args, ctx, _exec_field) class SpecifiedByUrlResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |type| + map_objects(objects) do |type| type.specified_by_url if type.kind.scalar? end end @@ -112,8 +112,8 @@ def resolve(objects, args, ctx, _exec_field) class ArgumentsResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) do |owner| - owner_args = ctx.query.types.arguments(owner) + map_objects(objects) do |owner| + owner_args = ctx.types.arguments(owner) owner_args = owner_args.reject(&:deprecation_reason) unless args["includeDeprecated"] owner_args end @@ -124,7 +124,7 @@ class ArgumentDefaultValueResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) builder = nil printer = nil - map_sources(objects) do |arg| + map_objects(objects) do |arg| next nil unless arg.default_value? builder ||= GraphQL::Language::DocumentFromSchemaDefinition.new(ctx.query.schema, context: ctx) @@ -136,7 +136,7 @@ def resolve(objects, args, ctx, _exec_field) class IsDeprecatedResolver < FieldResolver def resolve(objects, args, ctx, _exec_field) - map_sources(objects) { !!_1.deprecation_reason } + map_objects(objects) { !!_1.deprecation_reason } end end diff --git a/lib/graphql/cardinal/tracer.rb b/lib/graphql/cardinal/tracer.rb index 88b579a..66fc451 100644 --- a/lib/graphql/cardinal/tracer.rb +++ b/lib/graphql/cardinal/tracer.rb @@ -7,12 +7,12 @@ def initialize @time = nil end - def before_resolve_field(parent_type, field_name, sources_count, context) + def before_resolve_field(parent_type, field_name, objects_count, context) @time = Process.clock_gettime(Process::CLOCK_MONOTONIC) end - def after_resolve_field(parent_type, field_name, sources_count, context) - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @time) / sources_count + def after_resolve_field(parent_type, field_name, objects_count, context) + (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @time) / objects_count end end end