Skip to content

Commit e348dcc

Browse files
committed
Support interfaces in EntityMapping's
Closes gh-1356
1 parent 377e6d3 commit e348dcc

File tree

4 files changed

+59
-12
lines changed

4 files changed

+59
-12
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/federation/EntitiesDataFetcher.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,18 @@ final class EntitiesDataFetcher implements DataFetcher<Mono<DataFetcherResult<Li
5252

5353
private final Map<String, EntityHandlerMethod> handlerMethods;
5454

55+
/** schema object type to interface types it implements. */
56+
private final Map<String, String> objectToInterfaceMap;
57+
5558
private final HandlerDataFetcherExceptionResolver exceptionResolver;
5659

5760

5861
EntitiesDataFetcher(
59-
Map<String, EntityHandlerMethod> handlerMethods, HandlerDataFetcherExceptionResolver resolver) {
62+
Map<String, EntityHandlerMethod> handlerMethods, Map<String, String> objectToInterfaceMap,
63+
HandlerDataFetcherExceptionResolver resolver) {
6064

6165
this.handlerMethods = new LinkedHashMap<>(handlerMethods);
66+
this.objectToInterfaceMap = objectToInterfaceMap;
6267
this.exceptionResolver = resolver;
6368
}
6469

@@ -84,6 +89,10 @@ public Mono<DataFetcherResult<List<Object>>> get(DataFetchingEnvironment env) {
8489
continue;
8590
}
8691
EntityHandlerMethod handlerMethod = this.handlerMethods.get(type);
92+
if (handlerMethod == null) {
93+
String interfaceType = this.objectToInterfaceMap.get(type);
94+
handlerMethod = this.handlerMethods.get(interfaceType);
95+
}
8796
if (handlerMethod == null) {
8897
Exception ex = new RepresentationException(map, "No entity fetcher");
8998
monoList.add(resolveException(ex, env, null, index));

spring-graphql/src/main/java/org/springframework/graphql/data/federation/FederationSchemaFactory.java

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
import graphql.language.Argument;
3030
import graphql.language.BooleanValue;
3131
import graphql.language.Directive;
32+
import graphql.language.ObjectTypeDefinition;
33+
import graphql.language.Type;
3234
import graphql.language.TypeDefinition;
35+
import graphql.language.TypeName;
3336
import graphql.schema.DataFetcher;
3437
import graphql.schema.GraphQLSchema;
3538
import graphql.schema.TypeResolver;
@@ -180,18 +183,49 @@ public GraphQLSchema createGraphQLSchema(TypeDefinitionRegistry registry, Runtim
180183
* @param wiring the existing runtime wiring
181184
*/
182185
public SchemaTransformer createSchemaTransformer(TypeDefinitionRegistry registry, RuntimeWiring wiring) {
183-
checkEntityMappings(registry);
184-
Assert.state(this.typeResolver != null, "afterPropertiesSet not called");
186+
Assert.state(this.typeResolver != null, "Not initialized: was afterPropertiesSet called?");
187+
188+
Map<String, String> objectToInterfaceTypeMap = detectInterfaceImplementationTypes(registry);
189+
checkEntityMappings(registry, objectToInterfaceTypeMap);
190+
191+
EntitiesDataFetcher entitiesDataFetcher =
192+
new EntitiesDataFetcher(this.handlerMethods, objectToInterfaceTypeMap, getExceptionResolver());
193+
185194
return Federation.transform(registry, wiring)
186-
.fetchEntities(new EntitiesDataFetcher(this.handlerMethods, getExceptionResolver()))
195+
.fetchEntities(entitiesDataFetcher)
187196
.resolveEntityType(this.typeResolver);
188197
}
189198

190-
private void checkEntityMappings(TypeDefinitionRegistry registry) {
199+
/**
200+
* For all schema interface types referenced in entity mappings, return a
201+
* lookup from schema object type to the interface it implements.
202+
*/
203+
private Map<String, String> detectInterfaceImplementationTypes(TypeDefinitionRegistry registry) {
204+
Map<String, String> map = new LinkedHashMap<>();
205+
for (TypeDefinition<?> typeDef : registry.types().values()) {
206+
if (typeDef instanceof ObjectTypeDefinition objectDef) {
207+
for (Type<?> type : objectDef.getImplements()) {
208+
String interfaceName = ((TypeName) type).getName();
209+
if (this.handlerMethods.containsKey(interfaceName)) {
210+
String objectTypeName = objectDef.getName();
211+
String existingInterface = map.put(objectTypeName, interfaceName);
212+
Assert.state(existingInterface == null, () ->
213+
"Object type '" + objectTypeName + "' implements two EntityMapping interfaces: '" +
214+
interfaceName + "' and '" + existingInterface + "'.");
215+
}
216+
}
217+
}
218+
}
219+
return map;
220+
}
221+
222+
private void checkEntityMappings(TypeDefinitionRegistry registry, Map<String, String> objectToInterfaceTypeMap) {
191223
List<String> unmappedEntities = new ArrayList<>();
192224
for (TypeDefinition<?> type : registry.types().values()) {
193225
type.getDirectives().forEach((directive) -> {
194-
if (isResolvableKeyDirective(directive) && !this.handlerMethods.containsKey(type.getName())) {
226+
if (isResolvableKeyDirective(directive) &&
227+
!this.handlerMethods.containsKey(type.getName()) &&
228+
!objectToInterfaceTypeMap.containsKey(type.getName())) {
195229
unmappedEntities.add(type.getName());
196230
}
197231
});

spring-graphql/src/test/java/org/springframework/graphql/data/federation/EntityMappingInvocationTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ void dataLoader() {
195195
@Test
196196
void unmappedEntity() {
197197
assertThatIllegalStateException().isThrownBy(() -> executeWith(EmptyController.class, Map.of()))
198-
.withMessage("Unmapped entity types: 'Book'");
198+
.withMessage("Unmapped entity types: 'Media', 'Book'");
199199
}
200200

201201
private static ResponseHelper executeWith(Class<?> controllerClass, Map<String, Object> variables) {
@@ -247,7 +247,7 @@ private static TestExecutionGraphQlService graphQlService(Class<?> controllerCla
247247
private static class BookController {
248248

249249
@Nullable
250-
@EntityMapping
250+
@EntityMapping("Media")
251251
public Book book(@Argument int id, Map<String, Object> map) {
252252

253253
assertThat(map).hasSize(2)
@@ -282,7 +282,7 @@ private static class BookListController {
282282

283283
private final BookBatchService batchService = new BookBatchService();
284284

285-
@EntityMapping
285+
@EntityMapping("Media")
286286
public List<Book> book(@Argument List<Integer> idList, List<Map<String, Object>> representations) {
287287
return this.batchService.book(idList, representations);
288288
}
@@ -305,7 +305,7 @@ private static class BookFluxController {
305305

306306
private final BookBatchService batchService = new BookBatchService();
307307

308-
@EntityMapping
308+
@EntityMapping("Media")
309309
public Flux<Book> book(@Argument List<Integer> idList, List<Map<String, Object>> representations) {
310310
return Flux.fromIterable(this.batchService.book(idList, representations));
311311
}
@@ -334,7 +334,7 @@ public DataLoaderBookController(BatchLoaderRegistry batchLoaderRegistry) {
334334
}
335335

336336
@Nullable
337-
@EntityMapping
337+
@EntityMapping("Media")
338338
public Future<Book> book(@Argument int id, DataLoader<Integer, Book> dataLoader) {
339339
return dataLoader.load(id);
340340
}

spring-graphql/src/test/resources/books/federation-schema.graphqls

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@extends", "@external"] )
22

3-
type Book @key(fields: "id") @extends {
3+
interface Media @key(fields: "id") {
4+
id: ID! @external
5+
}
6+
7+
type Book implements Media @key(fields: "id") @extends {
48
id: ID! @external
59
author: Author
610
publisher: Publisher

0 commit comments

Comments
 (0)