Skip to content

Commit 7ed5e11

Browse files
authored
feature: auto-populate entity type resolver for non-resolvable entities (#285)
Whenever all entites are non-resolvable locally (i.e. `@key` specifies `resolvable:false`), then corresponding resolvers should never be invoked locally and users shouldn't have to specify fake ones. Changes: * default fake `_Entity` type resolver will be automatically created if it is not provided and all entities are non-resolvable * default fake `_Entity` type resolver will throw `IllegalStateException` if invoked at runtime * `_entities` data fetcher does not have to be specified if all entities are non-resolvable
1 parent 9ce2cb9 commit 7ed5e11

File tree

4 files changed

+172
-29
lines changed

4 files changed

+172
-29
lines changed

graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/SchemaTransformer.java

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55

66
import com.apollographql.federation.graphqljava.exceptions.MissingKeyException;
77
import graphql.GraphQLError;
8+
import graphql.language.BooleanValue;
89
import graphql.language.StringValue;
910
import graphql.schema.Coercing;
1011
import graphql.schema.DataFetcher;
1112
import graphql.schema.DataFetcherFactory;
1213
import graphql.schema.FieldCoordinates;
14+
import graphql.schema.GraphQLAppliedDirectiveArgument;
15+
import graphql.schema.GraphQLArgument;
1316
import graphql.schema.GraphQLCodeRegistry;
1417
import graphql.schema.GraphQLDirectiveContainer;
1518
import graphql.schema.GraphQLInterfaceType;
@@ -73,9 +76,7 @@ public SchemaTransformer setFederation2(boolean isFederation2) {
7376
}
7477

7578
@NotNull
76-
public final GraphQLSchema build() throws SchemaProblem {
77-
final List<GraphQLError> errors = new ArrayList<>();
78-
79+
public GraphQLSchema build() throws SchemaProblem {
7980
// Make new Schema
8081
final GraphQLSchema.Builder newSchema = GraphQLSchema.newSchema(originalSchema);
8182

@@ -98,31 +99,7 @@ public final GraphQLSchema build() throws SchemaProblem {
9899
newSchema.query(newQueryType.build());
99100

100101
final GraphQLCodeRegistry.Builder newCodeRegistry =
101-
GraphQLCodeRegistry.newCodeRegistry(originalSchema.getCodeRegistry());
102-
103-
if (!entityTypeNames.isEmpty()) {
104-
if (entityTypeResolver != null) {
105-
newCodeRegistry.typeResolver(_Entity.typeName, entityTypeResolver);
106-
} else {
107-
if (!newCodeRegistry.hasTypeResolver(_Entity.typeName)) {
108-
errors.add(new FederationError("Missing a type resolver for _Entity"));
109-
}
110-
}
111-
112-
final FieldCoordinates _entities =
113-
FieldCoordinates.coordinates(originalQueryType.getName(), _Entity.fieldName);
114-
if (entitiesDataFetcher != null) {
115-
newCodeRegistry.dataFetcher(_entities, entitiesDataFetcher);
116-
} else if (entitiesDataFetcherFactory != null) {
117-
newCodeRegistry.dataFetcher(_entities, entitiesDataFetcherFactory);
118-
} else if (!newCodeRegistry.hasDataFetcher(_entities)) {
119-
errors.add(new FederationError("Missing a data fetcher for _entities"));
120-
}
121-
}
122-
123-
if (!errors.isEmpty()) {
124-
throw new SchemaProblem(errors);
125-
}
102+
updateCodeRegistryWithEntityResolvers(entityTypeNames);
126103

127104
// expose the schema as _service.sdl
128105
newCodeRegistry.dataFetcher(
@@ -168,7 +145,7 @@ Set<String> getFederatedEntities() {
168145
.map(type -> (GraphQLObjectType) type)
169146
.forEach(
170147
type ->
171-
type.getInterfaces().stream()
148+
type.getInterfaces()
172149
.forEach(
173150
intf -> {
174151
if (interfaceEntities.contains(intf)) {
@@ -224,6 +201,89 @@ private Set<String> retrieveFieldSets(GraphQLDirectiveContainer type) {
224201
return fieldSets;
225202
}
226203

204+
private GraphQLCodeRegistry.Builder updateCodeRegistryWithEntityResolvers(
205+
Set<String> entityTypeNames) {
206+
final List<GraphQLError> errors = new ArrayList<>();
207+
final GraphQLCodeRegistry.Builder newCodeRegistry =
208+
GraphQLCodeRegistry.newCodeRegistry(originalSchema.getCodeRegistry());
209+
210+
if (!entityTypeNames.isEmpty()) {
211+
final boolean areEntitiesResolvable = resolvableEntitiesExist(entityTypeNames);
212+
if (entityTypeResolver != null) {
213+
newCodeRegistry.typeResolver(_Entity.typeName, entityTypeResolver);
214+
} else if (!areEntitiesResolvable) {
215+
// add fake entity union resolver as this type will never be resolved locally
216+
newCodeRegistry.typeResolver(
217+
_Entity.typeName,
218+
env -> {
219+
throw new IllegalStateException(
220+
"_Entity type resolver should never be called on non-resolvable entities");
221+
});
222+
} else if (!newCodeRegistry.hasTypeResolver(_Entity.typeName)) {
223+
errors.add(new FederationError("Missing a type resolver for _Entity"));
224+
}
225+
226+
if (areEntitiesResolvable) {
227+
// need _entities resolver
228+
final FieldCoordinates _entities =
229+
FieldCoordinates.coordinates(
230+
originalSchema.getQueryType().getName(), _Entity.fieldName);
231+
if (entitiesDataFetcher != null) {
232+
newCodeRegistry.dataFetcher(_entities, entitiesDataFetcher);
233+
} else if (entitiesDataFetcherFactory != null) {
234+
newCodeRegistry.dataFetcher(_entities, entitiesDataFetcherFactory);
235+
} else if (!newCodeRegistry.hasDataFetcher(_entities)) {
236+
errors.add(new FederationError("Missing a data fetcher for _entities"));
237+
}
238+
}
239+
}
240+
241+
if (!errors.isEmpty()) {
242+
throw new SchemaProblem(errors);
243+
}
244+
245+
return newCodeRegistry;
246+
}
247+
248+
private boolean resolvableEntitiesExist(Set<String> entityNames) {
249+
return entityNames.stream()
250+
.anyMatch(
251+
entity -> {
252+
GraphQLObjectType entityObject = (GraphQLObjectType) originalSchema.getType(entity);
253+
boolean isResolvable =
254+
entityObject.getAppliedDirectives(FederationDirectives.keyName).stream()
255+
.anyMatch(
256+
key -> {
257+
GraphQLAppliedDirectiveArgument resolvable =
258+
key.getArgument("resolvable");
259+
if (resolvable != null) {
260+
BooleanValue resolvableValue =
261+
(BooleanValue) resolvable.getArgumentValue().getValue();
262+
return resolvableValue == null || resolvableValue.isValue();
263+
} else {
264+
return true;
265+
}
266+
});
267+
268+
if (!isResolvable) {
269+
// fallback to also verify old directive definitions
270+
return entityObject.getDirectives(FederationDirectives.keyName).stream()
271+
.anyMatch(
272+
key -> {
273+
GraphQLArgument resolvable = key.getArgument("resolvable");
274+
if (resolvable != null) {
275+
BooleanValue resolvableValue =
276+
(BooleanValue) resolvable.getArgumentValue().getValue();
277+
return resolvableValue == null || resolvableValue.isValue();
278+
}
279+
return true;
280+
});
281+
} else {
282+
return true;
283+
}
284+
});
285+
}
286+
227287
/**
228288
* Generate Apollo Federation v1 compatible SDL that should be returned from `_service { sdl }`
229289
* query.

graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,19 @@ public void verifyFederationV2Transformation_interfaceEntity() {
286286
verifyFederationTransformation("schemas/interfaceEntity.graphql", runtimeWiring, true);
287287
}
288288

289+
@Test
290+
public void verifyFederationV2Transformation_nonResolvableKey_doesNotRequireResolvers() {
291+
final String originalSDL = FileUtils.readResource("schemas/nonResolvableKey.graphql");
292+
final RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build();
293+
final GraphQLSchema federatedSchema = Federation.transform(originalSDL, runtimeWiring).build();
294+
295+
final String expectedFederatedSchemaSDL =
296+
FileUtils.readResource("schemas/nonResolvableKey_federated.graphql");
297+
FederatedSchemaVerifier.verifySchemaSDL(federatedSchema, expectedFederatedSchemaSDL, true);
298+
FederatedSchemaVerifier.verifySchemaContainsServiceFederationType(federatedSchema);
299+
FederatedSchemaVerifier.verifyServiceSDL(federatedSchema, expectedFederatedSchemaSDL);
300+
}
301+
289302
private GraphQLSchema verifyFederationTransformation(
290303
String schemaFileName, boolean isFederationV2) {
291304
final RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
2+
3+
type Product {
4+
id: ID!
5+
description: String!
6+
createdBy: User
7+
}
8+
9+
type Query {
10+
product(id: ID!): Product
11+
}
12+
13+
type User @key(fields: "email", resolvable: false) {
14+
email: ID!
15+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
schema @link(import : ["@key"], url : "https://specs.apollo.dev/federation/v2.3"){
2+
query: Query
3+
}
4+
5+
directive @federation__composeDirective(name: String!) repeatable on SCHEMA
6+
7+
directive @federation__extends on OBJECT | INTERFACE
8+
9+
directive @federation__external on OBJECT | FIELD_DEFINITION
10+
11+
directive @federation__interfaceObject on OBJECT
12+
13+
directive @federation__override(from: String!) on FIELD_DEFINITION
14+
15+
directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION
16+
17+
directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION
18+
19+
directive @federation__shareable repeatable on OBJECT | FIELD_DEFINITION
20+
21+
directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
22+
23+
directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
24+
25+
directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA
26+
27+
directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
28+
29+
union _Entity = User
30+
31+
type Product {
32+
createdBy: User
33+
description: String!
34+
id: ID!
35+
}
36+
37+
type Query {
38+
_entities(representations: [_Any!]!): [_Entity]!
39+
_service: _Service!
40+
product(id: ID!): Product
41+
}
42+
43+
type User @key(fields : "email", resolvable : false) {
44+
email: ID!
45+
}
46+
47+
type _Service {
48+
sdl: String!
49+
}
50+
51+
scalar _Any
52+
53+
scalar federation__FieldSet
54+
55+
scalar link__Import

0 commit comments

Comments
 (0)