Repository navigation
Support of Reactor's types like Mono<T> and Flux<T>? #103
Description
I'm building a Spring Boot 2 project with Reactive Web (Spring WebFlux), which relies on Reactor. My services are thus returning Mono<T>s and Flux<T>s.
The problem is that I can't manage to make my GraphQLSchema handle those types correctly.
For example, say I have the following schema:
type Query {
article(
id: Long!
): Article!
}
type Article {
id: Long!
name: String!
}
And the following QueryResolver:
public class QueryResolver implements GraphQLQueryResolver {
private final ArticleService articleService;
public QueryResolver(ArticleService articleService) {
this.articleService = articleService;
}
public Mono<Article> getArticle(long id) {
return articleService.findOneById(id);
}
}Building the schema:
GraphQLSchema schema = SchemaParser.newParser()
.file("schema.graphqls")
.resolvers(queryResolver)
.build()
.makeExecutableSchema();
GraphQL build = GraphQL.newGraphQL(schema).build();When running a query:
ExecutionResult executionResult = build.execute("...");I catch the following exception:
TypeClassMatcher$RawClassRequiredForGraphQLMappingException: Type reactor.core.publisher.Mono<my.pkg.Article> cannot be mapped to a GraphQL type! Since GraphQL-Java deals with erased types at runtime, only non-parameterized classes can represent a GraphQL type. This allows for reverse-lookup by java class in interfaces and union types.
I then tried to register a SchemaParserOptions.GenericWrapper for the Mono<T> type:
SchemaParserOptions options = SchemaParserOptions.newOptions()
.genericWrappers(new SchemaParserOptions.GenericWrapper(Mono.class, 0))
.build();After having passed these options to the SchemaParser above, when running a query, I now catch the following exception:
ResolverError: Expected source object to be an instance of 'my.pkg.Article' but instead got 'reactor.core.publisher.MonoCompletionStage'
I wanted to try to register a SchemaParserOptions.GenericWrapper for MonoCompletionStage as well, but the class is only visible package-wise.
Any idea how I could proceed?
Just a naive idea of mine: couldn't inheritance be checked when the SchemaParserOptions.GenericWrappers are used? As MonoCompletionStage<T> is a subtype of Mono<T>, registering a SchemaParserOptions.GenericWrapper for Mono<T>could then take into account the subtypes of Mono<T> somehow... Note that I have no idea of the technical involvements of this ;)
In this case, because you're already registering a SchemaParserOptions.GenericWrapper for Publisher<T> if I use .useDefaultGenericWrappers(true), I even wouldn't have to register a SchemaParserOptions.GenericWrapper for Mono<T> as it extends Publisher<T>.
In the meantime, I'm converting my Mono<T> into a CompletableFuture<T>:
public CompletableFuture<Article> getArticle(long id) {
return articleService.findOneById(id).toFuture();
}And then converting the query result back into a Mono<T> to make it WebFlux-compliant:
Mono.fromFuture(result);
Activity
apottere commentedon Jan 12, 2018
Makes sense, I can look into checking for a subclass of a generic wrapper.
sp00m commentedon Feb 21, 2018
Little update: I've started playing with
@Batchedfetchers to avoid "select n+1", it would be pretty elegant if I could simply return aFluxfrom these! For example, say oneArticleis linked to oneBlog:In the above example,
blogService.findByArticles(articles)returns aFlux<Blog>(pushing as manyBlogs as inputtedArticles, plus making sure the inputtedArticles order matches the the oneBlogs have been pushed into theFlux... quite a lot of conditions, but this isn'tgraphql-java-tools's matter anyway).A less-but-still elegant API would be to use a
Mono, e.g.:What do you think? I'm afraid all this is going kind of out of scope for
graphql-java-toolsthough, making it quite tightly coupled with Reactor...sp00m commentedon Feb 21, 2018
Wait, thinking of my original issue again, there's something I don't fully understand: registering a
GenericWrapperforMonoactually made the engine successfully validate the schema when creating theGraphQLSchemainstance. The issue I then faced was at query execution time, because I guess the engine simply doesn't know how to get the actual wrapped value out of theMono(more specifically out of theMonoCompletionStage, subtype ofMono), which is kind of expected as I haven't registered any kind of converter, am I wrong?If this is true, then what's the use case of actually registering custom
GenericWrappers? Because the engine won't ever know how to get the actual value out of it, as it's indeed custom... What did I misunderstand here?sp00m commentedon Feb 21, 2018
Actually, maybe there something to do around
MethodFieldResolverDataFetcher#get(...)?Because, if I understand correctly, this line basically unwraps an
Optional(if the returned value is anOptionalobviously), which is actually what could be done for aMono(i.e. converting it into aFuturethanks toMono#toFuture(), as the engine already knows how to handleFutures).So maybe, instead of having this hardcoded only for
Optional, we could think of a way for integrating apps to register custom converters, just like you can currently do forGenericWrappers? Then, thisOptionalconversion could be registered by default, among the possible custom ones.@apottere, what do you think of all this?
Or a possibly even better approach could be to make these
GenericWrappers optionally aware of how to convert from their wrapping type to their wrapped type.Optionalcould then be aGenericWrapperdirectly, which would then not only make the above conversion more dynamic, but also the schema check.