Description
Hi! Big spring-graphql fan here. Thanks for building such great GraphQL tooling. I have a question related to federation.
Context
I work with a handful of Spring Boot apps that use GraphQL federation. An app's subgraph may contain references to types owned by a different subgraph, as described here. To borrow an example from the spring-graphql federation tests, the subgraph that owns a Book
type may contain a reference to a different subgraph that owns and knows more details about a Publisher
type.
type Book @key(fields: "id") @extends {
id: ID! @external
author: Author
publisher: Publisher
}
type Publisher @key(fields: "id", resolvable: false) {
id: ID! @external
}
Possible Problem
After upgrading to spring-graphql 1.4.0, we began seeing a failure on application startup that might look like this:
java.lang.IllegalStateException: Failed to load ApplicationContext for ...
...
Caused by: java.lang.IllegalStateException: Unmapped entity types: 'Publisher'
at org.springframework.graphql.data.federation.FederationSchemaFactory.checkEntityMappings(FederationSchemaFactory.java:202)
at org.springframework.graphql.data.federation.FederationSchemaFactory.createSchemaTransformer(FederationSchemaFactory.java:183)
...
I believe this is related to a new feature described by #1088. I support failing fast due to a missing @EntityMapping
method rather than risking runtime failures. However, I believe it is legitimate that types with the @key(..., resolvable: false)
directive would not have an @EntityMapping
method for that type.
Our workaround is to somewhat artificially provide mapping methods that return stubs representing those references, something like this:
@Controller
public class UnresolvableReferenceController {
public record Publisher(String id) {}
@EntityMapping
public Publisher publisher(String id) {
return new Publisher(id);
}
}
Possible Solution
Perhaps unresolvable types should be exempt from the check that's involved here? I've been playing with a solution in FederationSchemaFactory
that looks like the code below. However, I'm certainly open to your guidance if this wouldn't make sense.
private void checkEntityMappings(TypeDefinitionRegistry registry) {
List<String> unmappedEntities = new ArrayList<>();
for (TypeDefinition<?> type : registry.types().values()) {
if (isEntityMappingExpected(type) && !this.handlerMethods.containsKey(type.getName())) {
unmappedEntities.add(type.getName());
}
}
if (!unmappedEntities.isEmpty()) {
throw new IllegalStateException("Unmapped entity types: " +
unmappedEntities.stream().collect(Collectors.joining("', '", "'", "'")));
}
}
/**
* Determine if a handler method is expected for this type: there is at least one '@key' directive
* whose 'resolvable' argument evaluates to true (either explicitly, or if the argument is not set).
*
* @param type The type to inspect.
* @return true if a handler method is expected for this type
*/
private boolean isEntityMappingExpected(TypeDefinition<?> type) {
List<Directive> keyDirectives = type.getDirectives("key");
return !keyDirectives.isEmpty() && keyDirectives.stream()
.anyMatch(keyDirective -> {
Argument resolvableArg = keyDirective.getArgument("resolvable");
return resolvableArg == null ||
(resolvableArg.getValue() instanceof BooleanValue) && ((BooleanValue) resolvableArg.getValue()).isValue();
});
}
Thank you in advance for your consideration!