Skip to content

Commit

Permalink
feat: adds support for Interface Entities
Browse files Browse the repository at this point in the history
Context: In Federation v2.3 there was added support for interfaces to be
Entities. There are two ways to consume this new feature.

1. Extend an Interface Entity with the `@interfaceObject` directive,
   when the Entity is defined by a different subgraph.

2. Implement the Interface Entity with its implementing Type Entitites.

There was previos work into getting the first part covered in this gem,
but we were still lacking support for the 2nd part. This work address
that.

The main issue here is the fact that an `Union` in `GraphQL` can't be an
`Interface` according to the [spec](https://spec.graphql.org/October2021/#sec-Unions.Type-Validation), but at the same time, according to the Apollo Federation [spec](https://www.apollographql.com/docs/federation/federated-types/interfaces), an interface can be an Entity, and an Entity is an Union. Therefore, we have to extend the validation (`assert_valid_union_member`) for the Entity union to allow `Interfaces` (more exactly `Modules`) as possible types.
  • Loading branch information
moonflare committed Sep 7, 2023
1 parent ab31187 commit 04dbe48
Show file tree
Hide file tree
Showing 6 changed files with 678 additions and 6 deletions.
5 changes: 2 additions & 3 deletions lib/apollo-federation/entities_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,12 @@ def _entities(representations:)

# TODO: Use warden or schema?
type = context.warden.get_type(typename)
if type.nil? || type.kind != GraphQL::TypeKinds::OBJECT
if type.nil? || (type.kind != GraphQL::TypeKinds::OBJECT && type.kind != GraphQL::TypeKinds::INTERFACE)
# TODO: Raise a specific error class?
raise "The _entities resolver tried to load an entity for type \"#{typename}\"," \
' but no object type of that name was found in the schema'
end

# TODO: What if the type is an interface?
type_class = class_of_type(type)

if type_class.underscore_reference_keys
Expand Down Expand Up @@ -89,7 +88,7 @@ def _entities(representations:)
private

def class_of_type(type)
if defined?(GraphQL::ObjectType) && type.is_a?(GraphQL::ObjectType)
if (defined?(GraphQL::ObjectType) && type.is_a?(GraphQL::ObjectType)) || (defined?(GraphQL::InterfaceType) && type.is_a?(GraphQL::InterfaceType))
type.metadata[:type_class]
else
type
Expand Down
20 changes: 20 additions & 0 deletions lib/apollo-federation/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,25 @@ class Entity < GraphQL::Schema::Union
def self.resolve_type(object, context)
context[object]
end

# The main issue here is the fact that an union in GraphQL can't be an interface
# according to the [spec](https://spec.graphql.org/October2021/#sec-Unions.Type-Validation),
# but at the same time, according to the Federation spec, an interface can be an Entity,
# and an Entity is an union. Therefore, we have to extend this validation to allow interfaces
# (more precisely Modules) as possible types.
def self.assert_valid_union_member(type_defn)
case type_defn
when Class
if !type_defn.kind.object?
raise ArgumentError, "Union possible_types can only be object types (not #{type_defn.kind.name}, #{type_defn.inspect})"
end
when Module
# It's an interface entity, defined as a module
when String, GraphQL::Schema::LateBoundType
# Ok - assume it will get checked later
else
raise ArgumentError, "Union possible_types can only be class-based GraphQL types (not #{type_defn.inspect} (#{type_defn.class.name}))."
end
end
end
end
4 changes: 2 additions & 2 deletions lib/apollo-federation/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ def schema_entities
# Walk through all of the types and determine which ones are entities (any type with a
# "key" directive)
types_schema.send(:non_introspection_types).values.flatten.select do |type|
# TODO: Interfaces can have a key...
type.include?(ApolloFederation::Object) &&
# TODO: Find Objects that implement interfaces that are entities. Make sure they are also entities.
(type.include?(ApolloFederation::Object) || type.include?(ApolloFederation::Interface)) &&
type.federation_directives&.any? { |directive| directive[:name] == 'key' }
end
end
Expand Down
Loading

0 comments on commit 04dbe48

Please sign in to comment.