Problem statement: Need to write more code for implementing federated entity Data Fetcher using Spring for GraphQL framework.
Apollo-federation-jvm-spring-example: Spring GraphQL Federation Example
Expectation: New mapping annotation like @SchemaMapping to handle federated entity Data Fetcher
Our Custom Approach: To separate Data Fetchers based on cencerns, we followed the below approach. With this approach,
- Teams can concentrate on their business logic.
- Improve developer experience as the GraphQlConfig implementation can be done once in a common lib
- Create an Interface
public interface EntityDataFetcher {
public static final String TYP_NAME = "__typename";
public String getTypeName();
public Class<?> getTypeClass();
public Object fetch(@NotNull Map<String, Object> reference);
}
- Sample entity DataFetcher implementation
@Component
public class SampleDataFetcher implements EntityDataFetcher {
public static final String SCHEMA_TYP_NM = "Sample";
@Autowired
private SampleService sampleService;
@Override
public String getTypeName() {
return SCHEMA_TYP_NM; // Graph type, defined in schema
}
@Override
public Class<?> getTypeClass() {
return SampleResponseDTO.class; // Return object class, return by the data fetcher
}
@Override
public Object fetch(@NotNull Map<String, Object> reference) {
// Business logic to retrieve Sample entity
}
}
- GraphQlConfig implementation to register DataFetchers
@Configuration
public class GraphQlConfig {
@Autowired(required=false)
private List<EntityDataFetcher> dataFetchers;
@Bean
public GraphQlSourceBuilderCustomizer federationTransform() {
return builder -> builder.schemaFactory((registry, wiring) ->
Federation.transform(registry, wiring)
.fetchEntities(getEntityDataFetchers())
.resolveEntityType(getClassNameTypeResolver())
.build()
);
}
private DataFetcher<?> getEntityDataFetchers() {
Map<String, EntityDataFetcher> map = getEntityDataFetchersMap();
DataFetcher<?> entityDataFetcher = env -> {
List<Map<String, Object>> representations = env.getArgument(_Entity.argumentName);
return representations.stream().map(representation -> {
final String typeName = (String) representation.get(EntityDataFetcher.TYP_NAME);
if (map.containsKey(typeName)) {
return map.get(typeName).fetch(representation);
} else {
return null;
}
}).collect(Collectors.toList());
};
return entityDataFetcher;
}
private ClassNameTypeResolver getClassNameTypeResolver() {
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
if (dataFetchers != null) {
dataFetchers.stream().forEach(d -> {
classNameTypeResolver.addMapping(d.getTypeClass(), d.getTypeName());
});
}
return classNameTypeResolver;
}
private Map<String, EntityDataFetcher> getEntityDataFetchersMap() {
Map<String, EntityDataFetcher> map = null;
if (dataFetchers != null) {
map = dataFetchers.stream().collect(Collectors.toMap(EntityDataFetcher::getTypeName, d -> d));
} else {
map = Collections.emptyMap();
}
return map;
}
}