diff --git a/lib/graphql/schema/member/has_arguments.rb b/lib/graphql/schema/member/has_arguments.rb index 77c164d534..323850b997 100644 --- a/lib/graphql/schema/member/has_arguments.rb +++ b/lib/graphql/schema/member/has_arguments.rb @@ -359,7 +359,8 @@ def authorize_application_object(argument, id, context, loaded_application_objec if application_object.nil? nil else - maybe_lazy_resolve_type = context.schema.resolve_type(argument.loads, application_object, context) + arg_loads_type = argument.loads + maybe_lazy_resolve_type = context.schema.resolve_type(arg_loads_type, application_object, context) context.query.after_lazy(maybe_lazy_resolve_type) do |resolve_type_result| if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2 application_object_type, application_object = resolve_type_result @@ -368,10 +369,17 @@ def authorize_application_object(argument, id, context, loaded_application_objec # application_object is already assigned end - if !( - context.types.possible_types(argument.loads).include?(application_object_type) || - context.types.loadable?(argument.loads, context) - ) + passes_possible_types_check = if context.types.loadable?(arg_loads_type, context) + if arg_loads_type.kind.union? + # This union is used in `loads:` but not otherwise visible to this query + context.types.loadable_possible_types(arg_loads_type, context).include?(application_object_type) + else + true + end + else + context.types.possible_types(arg_loads_type).include?(application_object_type) + end + if !passes_possible_types_check err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object) application_object = load_application_object_failed(err) end diff --git a/lib/graphql/schema/visibility/migration.rb b/lib/graphql/schema/visibility/migration.rb index 01f65009b8..8639fa141a 100644 --- a/lib/graphql/schema/visibility/migration.rb +++ b/lib/graphql/schema/visibility/migration.rb @@ -112,6 +112,7 @@ def loaded_types :all_types_h, :fields, :loadable?, + :loadable_possible_types, :type, :arguments, :argument, diff --git a/lib/graphql/schema/visibility/profile.rb b/lib/graphql/schema/visibility/profile.rb index 1e6d2ae278..db609391c1 100644 --- a/lib/graphql/schema/visibility/profile.rb +++ b/lib/graphql/schema/visibility/profile.rb @@ -84,6 +84,8 @@ def initialize(name: nil, context:, schema:) @cached_arguments = Hash.new do |h, owner| h[owner] = non_duplicate_items(owner.all_argument_definitions, @cached_visible_arguments) end.compare_by_identity + + @loadable_possible_types = Hash.new { |h, union_type| h[union_type] = union_type.possible_types }.compare_by_identity end def field_on_visible_interface?(field, owner) @@ -249,6 +251,10 @@ def loadable?(t, _ctx) !@all_types[t.graphql_name] && @cached_visible[t] end + def loadable_possible_types(t, _ctx) + @loadable_possible_types[t] + end + def loaded_types @all_types.values end diff --git a/lib/graphql/schema/warden.rb b/lib/graphql/schema/warden.rb index d1241eb017..16be514978 100644 --- a/lib/graphql/schema/warden.rb +++ b/lib/graphql/schema/warden.rb @@ -72,6 +72,7 @@ def visible_type_membership?(tm, ctx); tm.visible?(ctx); end def interface_type_memberships(obj_t, ctx); obj_t.interface_type_memberships; end def arguments(owner, ctx); owner.arguments(ctx); end def loadable?(type, ctx); type.visible?(ctx); end + def loadable_possible_types(type, ctx); type.possible_types(ctx); end def visibility_profile @visibility_profile ||= Warden::VisibilityProfile.new(self) end @@ -106,6 +107,7 @@ def fields(type_defn); type_defn.all_field_definitions; end # rubocop:disable De def get_field(parent_type, field_name); @schema.get_field(parent_type, field_name); end def reachable_type?(type_name); true; end def loadable?(type, _ctx); true; end + def loadable_possible_types(union_type, _ctx); union_type.possible_types; end def reachable_types; @schema.types.values; end # rubocop:disable Development/ContextIsPassedCop def possible_types(type_defn); @schema.possible_types(type_defn, Query::NullContext.instance, false); end def interfaces(obj_type); obj_type.interfaces; end @@ -180,6 +182,10 @@ def loadable?(t, ctx) # TODO remove ctx here? @warden.loadable?(t, ctx) end + def loadable_possible_types(t, ctx) + @warden.loadable_possible_types(t, ctx) + end + def reachable_type?(type_name) !!@warden.reachable_type?(type_name) end @@ -204,7 +210,7 @@ def initialize(context:, schema:) @visible_possible_types = @visible_fields = @visible_arguments = @visible_enum_arrays = @visible_enum_values = @visible_interfaces = @type_visibility = @type_memberships = @visible_and_reachable_type = @unions = @unfiltered_interfaces = - @reachable_type_set = @visibility_profile = + @reachable_type_set = @visibility_profile = @loadable_possible_types = nil @skip_warning = schema.plugins.any? { |(plugin, _opts)| plugin == GraphQL::Schema::Warden } end @@ -229,6 +235,13 @@ def loadable?(type, _ctx) !reachable_type_set.include?(type) && visible_type?(type) end + def loadable_possible_types(union_type, _ctx) + @loadable_possible_types ||= read_through do |t| + t.possible_types # unfiltered + end + @loadable_possible_types[union_type] + end + # @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`) def get_type(type_name) @visible_types ||= read_through do |name| diff --git a/spec/graphql/schema/union_spec.rb b/spec/graphql/schema/union_spec.rb index e1b0961cef..0f019799f6 100644 --- a/spec/graphql/schema/union_spec.rb +++ b/spec/graphql/schema/union_spec.rb @@ -406,4 +406,58 @@ def unboxed_union end end end + + describe "use with loads:" do + class UnionLoadsSchema < GraphQL::Schema + class Image < GraphQL::Schema::Object + field :title, String + end + + class Video < GraphQL::Schema::Object + field :title, String + end + + class Post < GraphQL::Schema::Object + field :title, String + end + + class MediaItem < GraphQL::Schema::Union + possible_types Image, Video + end + + class Query < GraphQL::Schema::Object + field :media_item_type, String do + argument :id, ID, loads: MediaItem, as: :media_item + end + + def media_item_type(media_item:) + media_item[:type] + end + end + + query(Query) + + def self.object_from_id(id, ctx) + type, title = id.split("/") + { type: type, title: title } + end + + def self.resolve_type(abs_type, obj, ctx) + UnionLoadsSchema.const_get(obj[:type]) + end + end + + it "restricts to members of the union" do + query_str = "query($mediaId: ID!) { mediaItemType(id: $mediaId) }" + res = UnionLoadsSchema.execute(query_str, variables: { mediaId: "Image/Family Photo" }) + assert_equal "Image", res["data"]["mediaItemType"] + + res = UnionLoadsSchema.execute(query_str, variables: { mediaId: "Video/Christmas Pageant" }) + assert_equal "Video", res["data"]["mediaItemType"] + + res = UnionLoadsSchema.execute(query_str, variables: { mediaId: "Post/Year in Review" }) + assert_nil res["data"]["mediaItemType"] + assert_equal ["No object found for `id: \"Post/Year in Review\"`"], res["errors"].map { |e| e["message"] } + end + end end