spring-graphql icon indicating copy to clipboard operation
spring-graphql copied to clipboard

Support custom argument resolvers for @SchemaMapping methods

Open marceloverdijk opened this issue 2 years ago • 5 comments

As discussed in https://github.com/spring-projects/spring-graphql/issues/250 this is feature request for support of custom argument resolvers in GraphQL controllers.

E.g. to support

@QueryMapping
public Page<Season> seasons(Pageable pageable) {
    int first = pageable.getFirst();
    int offset = pageable.getOffset();
    ..
}

The plain example as above could already be implemented using a @ProjectedPayload

@ProjectedPayload
public interface Pageable {

    int getFirst();
    int getOffset();
}

(Note: using the @Argument annotation for above case does not work me, see https://github.com/spring-projects/spring-graphql/issues/258)

But supporting custom argument resolvers could offer more flexibility I was looking for like the ability to use a custom @interface to provide defaults , e.g.

@QueryMapping
public Page<Season> seasons(@PageableDefault(first=10, offset=0) Pageable pageable) {
    ..
}

marceloverdijk avatar Jan 15 '22 14:01 marceloverdijk

@marceloverdijk I'm wondering if the changes for #258 addressed the case here with Pageable or do you have another use case in mind?

Generally, both @SchemaMapping and @BatchMapping methods could have custom resolvers but they resolve from different contexts, DataFetchingEnvironment vs BatchLoaderEnvironment, and can't share the same contract. I'm also not certain what this would be used for, considering that we expose more or less everything there is.

rstoyanchev avatar Mar 24 '22 12:03 rstoyanchev

Hi @rstoyanchev #258 already helps a lot to be honest.

Having support for custom argument resolvers - like in e.g. MVC - I could do even more.

Besides binding the arguments to an object I could also apply some login when binding, like defining default values as can be seen in the example below. The idea below is build upon Spring Data's PageableHandlerMethodArgumentResolver.

@QueryMapping
public Page<Season> seasons(@PageableDefault(first=10, offset=0) Pageable pageable) {
    ..
}

marceloverdijk avatar Mar 28 '22 07:03 marceloverdijk

Thanks for clarifying. For the Pageable example, isn't the logic within it and hence orthogonal to the instantiation or is there something specific about the instantiation?

Of course it makes perfect sense to have such resolvers but for now it doesn't seem to be holding anything up, and with the two different types of handler methods and resolver contracts, I'd rather give this more time, before we find the best way to proceed.

rstoyanchev avatar Mar 29 '22 14:03 rstoyanchev

I understand.

For me it's about having more control to which default value to set in the argument resolver; it's coming from https://github.com/spring-projects/spring-graphql/issues/250

marceloverdijk avatar Mar 29 '22 14:03 marceloverdijk

I'm having a similar issue, but I'm not sure if a custom argument resolver can help:

Schema:

scalar ConnectionCursor

type Query {
    books(after: ConnectionCursor): [Book]
    authors(after: ConnectionCursor): [Author]
}

Controller:

@Controller
public class MyController{
    @QueryMapping
    Flux<Book> books(@Argument BookConnectionCursor after) {
        return Flux.empty();
    }

    @QueryMapping
    Flux<Author> authors(@Argument AuthorConnectionCursor after) {
        return Flux.empty();
    }
}

As you can see, in the schema there's only a generic ConnectionCursor scalar, but inside the controller code each @Argument should be able to use its ows specialized version.

When creating custom ConnectionCursor scalar with RuntimeWiringConfigurer it is easy to serialize, because I have type information. But with parseValue and parseLiteral I don't have any type information and no access to controller method and parameter, though I can't figure out if I should create a BookConnectionCursor or a AuthorConnectionCursor object.

Can a custom argument resolver solve this issue? (I guess there I should have the class type of the controller method argument)


EDIT: I just figured out that I can register org.springframework.core.convert.converter.Converter<String, BookConnectionCursor> and Converter<String, AuthorConnectionCursor> @Beans and then register the scalar as String Scalar:

@Bean
RuntimeWiringConfigurer configurer() {
    return builder -> builder
                .scalar(GraphQLScalarType.newScalar(Scalars.GraphQLString).name("ConnectionCursor").build());

}

Works as intended 😸 First the registered scalar creates a String from the input value, and then the GraphQlArgumentBinder looks for the matching Converter. Nice!

benneq avatar Jun 09 '22 18:06 benneq

I'm closing this as superseded by #325.

rstoyanchev avatar Oct 24 '22 10:10 rstoyanchev