Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Adds support for defining Interface entities resolvers #262

Closed
wants to merge 4 commits into from
Closed
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
73 changes: 71 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,20 @@ class Product < BaseObject
end
```

### The `@interfaceObject` directive (Apollo Federation v2.3)

[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#interfaceobject)

Call `interface_object` within your class definition:

```ruby
class Product < BaseObject
interface_object
key fields: :id
field :id, ID, null: false
end
```

### The `@tag` directive (Apollo Federation v2)

[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#tag)
Expand Down Expand Up @@ -327,7 +341,7 @@ Define a `resolve_reference` class method on your object. The method will be pas
class User < BaseObject
key fields: :user_id
field :user_id, ID, null: false

def self.resolve_reference(reference, context)
USERS.find { |user| user[:userId] == reference[:userId] }
end
Expand All @@ -341,7 +355,7 @@ class User < BaseObject
key fields: :user_id
field :user_id, ID, null: false
underscore_reference_keys true

def self.resolve_reference(reference, context)
USERS.find { |user| user[:user_id] == reference[:user_id] }
end
Expand All @@ -358,6 +372,61 @@ class BaseObject < GraphQL::Schema::Object
end
```

### Reference resolvers for Interfaces

[Apollo documentation](https://www.apollographql.com/docs/federation/federated-types/interfaces/#required-resolvers)

```ruby
module Product
include BaseInterface

key fields: :id
field :id, ID, null: false
field :title, String, null: true

definition_methods do
def resolve_type(obj, _ctx)
if obj.is_a?(Book)
BookType
elsif obj.is_a?(Movie)
MovieType
end

def resolve_reference(reference, _context)
PRODUCTS.find { |product| product[:id] == reference[:id] }
end
end
end

class BookType < BaseObject
implements Product
graphql_name 'Book'

key fields: :id
field :id, ID, null: false
field :title, String, null: true
field :pages, Integer, null: true

def self.resolve_reference(reference, _context)
BOOKS.find { |book| book[:id] == reference[:id] }
end
end

class MovieType < BaseObject
implements Product
graphql_name 'Movie'

key fields: :id
field :id, ID, null: false
field :title, String, null: true
field :minutes, Integer, null: true

def self.resolve_reference(reference, _context)
MOVIES.find { |movie| movie[:id] == reference[:id] }
end
end
```

### Tracing

To support [federated tracing](https://www.apollographql.com/docs/apollo-server/federation/metrics/):
Expand Down
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
13 changes: 13 additions & 0 deletions lib/apollo-federation/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,18 @@ 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 as possible types.
def self.assert_valid_union_member(type_defn)
if type_defn.is_a?(Module) &&
type_defn.included_modules.include?(ApolloFederation::Interface)
# It's an interface entity, defined as a module
else
super(type_defn)
end
end
end
end
12 changes: 12 additions & 0 deletions lib/apollo-federation/interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ def key(fields:, camelize: true)
],
)
end

def underscore_reference_keys(value = nil)
if value.nil?
if @underscore_reference_keys.nil?
find_inherited_value(:underscore_reference_keys, false)
else
@underscore_reference_keys
end
else
@underscore_reference_keys = value
end
end
end
end
end
56 changes: 50 additions & 6 deletions lib/apollo-federation/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,53 @@ def schema_entities
# infinite recursion
types_schema.orphan_types(original_query)

# 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) &&
type.federation_directives&.any? { |directive| directive[:name] == 'key' }
# Walk through all of the types and interfaces and determine which ones are entities
# (any type with a "key" directive)
# However, for interface entities, don't add them straight away, but first check that
# all implementing types of the interfaces are also entities.
federation_interfaces = []
interface_types_map = {}

entities_collection = types_schema.send(:non_introspection_types).values.flatten.select do |type|
# keep track of the interfaces -> type relations.
if type.respond_to?(:implements)
type.implements.each do |interface|
interface_types_map[interface.abstract_type.graphql_name] ||= []
interface_types_map[interface.abstract_type.graphql_name] << type.graphql_name
end
end

# Only add Type entities to the collection
# Interface entities will be added later if all implementing types are entities
if type.include?(ApolloFederation::Object) && includes_key_directive?(type)
true
elsif type.include?(ApolloFederation::Interface) && includes_key_directive?(type)
federation_interfaces << type
false
else
false
end
end

if federation_interfaces.any?
entity_names = entities_collection.map(&:graphql_name)

federation_interfaces.each do |interface|
members = interface_types_map.fetch(interface.graphql_name, [])
not_entity_members = members.reject { |member| entity_names.include?(member) }

# If all interface members are entities, it is valid so we add it to the collection
if not_entity_members.empty?
entities_collection << interface
else
raise "Interface #{interface.graphql_name} is not valid. " \
"Types `#{not_entity_members.join(', ')}` do not have a @key directive. " \
"All types that implement an interface with a @key directive must also have a @key directive."
end
end
end

entities_collection
end

def federation_query(query_obj)
Expand All @@ -110,6 +150,10 @@ def federation_query(query_obj)
klass.define_service_field
klass
end

def includes_key_directive?(type)
type.federation_directives&.any? { |directive| directive[:name] == 'key' }
end
end
end
end
Loading