Skip to content

Commit adf96d8

Browse files
nogatesmoonflare
authored andcommitted
fix: ensure all interface members are also entities
Every entity that implements an interface entity must include all `@keys` from the interface definition. Therefore, we add this extra validation to ensure that.
1 parent 8d511d6 commit adf96d8

File tree

3 files changed

+90
-7
lines changed

3 files changed

+90
-7
lines changed

lib/apollo-federation/entity.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def self.resolve_type(object, context)
1616
# is an union. Therefore, we have to extend this validation to allow interfaces as possible types.
1717
def self.assert_valid_union_member(type_defn)
1818
if type_defn.is_a?(Module) &&
19-
type_defn.included_modules.include?(ApolloFederation::Interface)
19+
type_defn.included_modules.include?(ApolloFederation::Interface)
2020
# It's an interface entity, defined as a module
2121
else
2222
super(type_defn)

lib/apollo-federation/schema.rb

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,59 @@ def schema_entities
7979
# infinite recursion
8080
types_schema.orphan_types(original_query)
8181

82-
# Walk through all of the types and determine which ones are entities (any type with a
83-
# "key" directive)
84-
types_schema.send(:non_introspection_types).values.flatten.select do |type|
85-
# TODO: Find Objects that implement interfaces that are entities. Make sure they are also entities.
86-
(type.include?(ApolloFederation::Object) || type.include?(ApolloFederation::Interface)) &&
87-
type.federation_directives&.any? { |directive| directive[:name] == 'key' }
82+
entities_collection, federation_entities, interface_types_map = collect_entitites(types_schema)
83+
84+
if federation_entities.any?
85+
entity_names = entities_collection.map(&:graphql_name)
86+
87+
federation_entities.each do |interface|
88+
members = interface_types_map.fetch(interface.graphql_name, [])
89+
not_entity_members = members.reject { |member| entity_names.include?(member) }
90+
91+
# If all interface members are entities, it is valid so we add it to the collection
92+
if not_entity_members.empty?
93+
entities_collection << interface
94+
else
95+
raise "Interface #{interface.graphql_name} is not valid. " \
96+
"Types `#{not_entity_members.join(', ')}` do not have a @key directive. " \
97+
'All types that implement an interface with a @key directive must also have a @key directive.'
98+
end
99+
end
88100
end
101+
102+
entities_collection
103+
end
104+
105+
# Walk through all of the types and interfaces and determine which ones are entities
106+
# (any type with a "key" directive)
107+
# However, for interface entities, don't add them straight away, but first check that
108+
# all implementing types of the interfaces are also entities.
109+
def collect_entitites(types_schema)
110+
federation_entities = []
111+
interface_types_map = {}
112+
113+
entities_collection = types_schema.send(:non_introspection_types).values.flatten.select do |type|
114+
# keep track of the interfaces -> type relations.
115+
if type.respond_to?(:implements)
116+
type.implements.each do |interface|
117+
interface_types_map[interface.abstract_type.graphql_name] ||= []
118+
interface_types_map[interface.abstract_type.graphql_name] << type.graphql_name
119+
end
120+
end
121+
122+
# Only add Type entities to the collection
123+
# Interface entities will be added later if all implementing types are entities
124+
if type.include?(ApolloFederation::Object) && includes_key_directive?(type)
125+
true
126+
elsif type.include?(ApolloFederation::Interface) && includes_key_directive?(type)
127+
federation_entities << type
128+
false
129+
else
130+
false
131+
end
132+
end
133+
134+
[entities_collection, federation_entities, interface_types_map]
89135
end
90136

91137
def federation_query(query_obj)
@@ -110,6 +156,10 @@ def federation_query(query_obj)
110156
klass.define_service_field
111157
klass
112158
end
159+
160+
def includes_key_directive?(type)
161+
type.federation_directives&.any? { |directive| directive[:name] == 'key' }
162+
end
113163
end
114164
end
115165
end

spec/apollo-federation/entities_field_interfaces_spec.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,39 @@
3636
end
3737

3838
context 'when an interface with the key directive exists' do
39+
context "when some of the types implementing the inteface don't have the key directive" do
40+
let(:offending_class) do
41+
end
42+
let(:query) do
43+
user_class = SpecTypes::User
44+
Class.new(SpecTypes::BaseObject) do
45+
graphql_name 'Query'
46+
field :user, user_class, null: true
47+
end
48+
end
49+
50+
it 'raises an error' do
51+
query_class = query
52+
53+
offending_class = Class.new(SpecTypes::BaseObject) do
54+
graphql_name 'Manager'
55+
implements SpecTypes::User
56+
57+
field :id, 'ID', null: false
58+
end
59+
60+
schema = Class.new(base_schema) do
61+
query query_class
62+
orphan_types SpecTypes::AdminType, offending_class
63+
end
64+
65+
expect { schema.to_definition }.to raise_error(
66+
'Interface User is not valid. Types `Manager` do not have a @key directive. ' \
67+
'All types that implement an interface with a @key directive must also have a @key directive.',
68+
)
69+
end
70+
end
71+
3972
context 'when a Query object is provided' do
4073
let(:query) do
4174
user_class = SpecTypes::User

0 commit comments

Comments
 (0)