Giter Club home page Giter Club logo

graphql-spqr-spring-boot-starter's Introduction

graphql-spqr-spring-boot-starter

Spring Boot starter powered by GraphQL SPQR

Join the chat at https://gitter.im/leangen/graphql-spqr StackOverflow Maven Central Javadoc Build Status License

Intro

GraphQL SPQR Spring Boot starter aims to make it dead simple to add a GraphQL API to any Spring Boot project.

  • Add @GraphQLApi to any Spring managed component, and you're good to go ๐Ÿš€
  • GraphQL endpoint available at /graphql by default
  • GraphQL Playground IDE (if enabled, see the properties below) available at /ide
  • Fully customizable in seconds by providing simple beans (any SPQR SPI can be exposed as a bean)

Project setup / Dependencies

To use this starter in a typical Spring Boot project, add the following dependencies to your project:

<dependencies>
  <dependency>
    <groupId>io.leangen.graphql</groupId>
    <artifactId>graphql-spqr-spring-boot-starter</artifactId>
    <version>1.0.0</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
</dependencies>

There's also a very basic sample project

Defining the operation sources (the beans that get exposed via the API)

All beans in Spring's application context annotated with @GraphqlApi are considered to be operation sources (a concept similar to Controller beans in Spring MVC). This annotation can be used in combination with @Component/@Service/@Repository or @Bean annotations, e.g.

    @Component
    @GraphQLApi
    private class UserService {
        //Query/mutation/subscription methods
        ...
    }

or

    @Bean
    @GraphQLApi
    public userService() {
        return new UserService(...);
    }

Choosing which methods get exposed through the API

To deduce which methods of each operation source class should be exposed as GraphQL queries/mutations/subscriptions, SPQR uses the concept of a ResolverBuilder (since each exposed method acts as a resolver function for a GraphQL operation). To cover the basic approaches SpqrAutoConfiguration registers a bean for each of the three built-in ResolverBuilder implementations:

  • AnnotatedResolverBuilder - exposes only the methods annotated by @GraphQLQuery, @GraphQLMutation or @GraphQLSubscription
  • PublicResolverBuilder - exposes all public methods from the operations source class (methods returning void are considered mutations)
  • BeanResolverBuilder - exposes all getters as queries and setters as mutations (getters returning Publisher<T> are considered subscriptions)
  • RecordResolverBuilder - exposes all record component accessors as queries (accessors returning Publisher<T> are considered subscriptions)

It is also possible to implement custom resolver builders by implementing the ResolverBuilder interface.

Resolver builders can be declared both globally and on the operation source level. If not sticking to the defaults, it is generally safer to explicitly customize on the operation source level, unless the rules are absolutely uniform across all operation sources. Customizing on both levels simultaneously will work but could prove tricky to control as your API grows.

Defaults:

  • For top-level beans: only AnnotatedResolverBuilder is registered,
  • For nested beans: AnnotatedResolverBuilder, BeanResolverBuilder and RecordResolverBuilder (applied to records only) are registered

Customizing resolver builders globally

To change the default resolver builders globally, implement and register a bean of type ExtensionProvider<ResolverBuilder>. A simplified example of this could be:

    @Bean
    public ExtensionProvider<GeneratorConfiguration, ResolverBuilder> resolverBuilderExtensionProvider() {
        return (config, current) -> {
            List<ResolverBuilder> resolverBuilders = new ArrayList<>();

            //add a custom subtype of PublicResolverBuilder that only exposes a method if it's called "greeting"
            resolverBuilders.add(new PublicResolverBuilder() {
                @Override
                protected boolean isQuery(Method method) {
                    return super.isQuery(method) && method.getName().equals("greeting");
                }
            });
            //add the default builder
            resolverBuilders.add(new AnnotatedResolverBuilder());

            return resolverBuilders;
        };
    }

This would add two resolver builders that apply to all operation sources. The First one exposes all public methods named greeting. The second is the inbuilt AnnotatedResolverBuilder (that exposes only the explicitly annotated methods). A quicker way to achieve the same would be:

    @Bean
    public ExtensionProvider<GeneratorConfiguration, ResolverBuilder> resolverBuilderExtensionProvider() {
        //prepend the custom builder to the provided list of defaults
        return (config, current) -> current.prepend(new PublicResolverBuilder() {
                @Override
                protected boolean isQuery(Method method) {
                    return super.isQuery(method) && method.getName().equals("greeting");
                }
            });
    };

Customizing the resolver builders for a specific operation source

To attach a resolver builder to a specific source (bean), use the @WithResolverBuilder annotation on it. This annotation also works both on the beans registered by @Component/@Service/@Repository or @Bean annotations.

As an example, we can expose the greeting query by using:

    @Component
    @GraphQLApi
    @WithResolverBuilder(BeanResolverBuilder.class) //exposes all getters
    private class MyOperationSource {
        public String getGreeting(){
            return "Hello world !";
        }
    }

or:

    @Bean
    @GraphQLApi
    //No explicit resolver builders declared, so AnnotatedResolverBuilder is used
    public MyOperationSource() {
        @GraphQLQuery(name = "greeting")
        public String getGreeting() {
            return "Hello world !";
        }
    }

It is also entirely possible to use more than one resolver builder on the same operation source e.g.

    @Component
    @GraphQLApi
    @WithResolverBuilder(BeanResolverBuilder.class)
    @WithResolverBuilder(AnnotatedResolverBuilder.class)
    private class MyOperationSource {
        //Exposed by BeanResolverBuilder because it's a getter
        public String getGreeting(){
            return "Hello world !";
        }

        //Exposed by AnnotatedResolverBuilder because it's annotated
        @GraphQLQuery
        public String personalGreeting(String name){
            return "Hello " + name + " !"; 
        }
    }

This way, both queries are exposed but in different ways. The same would work on a bean registered using the @Bean annotation.

Customize GraphQL type information

Sometimes it is useful to have an automated strategy for generating type names, descriptions and order of fields within the type. To do this SPQR uses TypeInfoGenerator on a global level. When using this starter the most convenient way is to wire a single bean of that type in the application context.

    @Bean
    public TypeInfoGenerator testTypeInfoGenerator() {
        return new TypeInfoGenerator() {
            @Override
            public String generateTypeName(AnnotatedType type, MessageBundle messageBundle) {
                return nameGenerationMethodLocalized(type, messageBundle);
            }

            @Override
            public String generateTypeDescription(AnnotatedType type, MessageBundle messageBundle) {
                return descriptionGenerationMethodLocalized(type, messageBundle);
            }

            @Override
            public String[] getFieldOrder(AnnotatedType type, MessageBundle messageBundle) {
                return fieldOrderGenerationMethodLocalized(type, messageBundle);
            }

        };
    }

Advanced config

Available Properties

Property Default Value
graphql.spqr.base-packages n/a
graphql.spqr.abstract-input-type-resolution false
graphql.spqr.relay.enabled false
graphql.spqr.relay.mutation-wrapper n/a
graphql.spqr.relay.mutation-wrapper-description n/a
graphql.spqr.relay.connection-check-relaxed false
graphql.spqr.relay.spring-data-compatible false
graphql.spqr.http.enabled true
graphql.spqr.http.endpoint /graphql
graphql.spqr.http.mvc.executor async
graphql.spqr.ws.enabled true
graphql.spqr.ws.endpoint n/a
graphql.spqr.ws.send-time-limit 10000
graphql.spqr.ws.send-buffer-size-limit 512 * 1024
graphql.spqr.ws.allowed-origins *
graphql.spqr.ws.keep-alive.enabled false
graphql.spqr.ws.keep-alive.interval-millis 10000
graphql.spqr.gui.enabled true
graphql.spqr.gui.endpoint /gui
graphql.spqr.gui.target-endpoint n/a
graphql.spqr.gui.target-ws-endpoint n/a
graphql.spqr.gui.page-title GraphQL Playground

Customize mapping of GraphQL values to Java values

Object in charge of doing this in SPQR is ValueMapperFactory. Again the simplest way to make use of this when using the starter is to wire a single bean of this type into the application context.

    @Bean
    public ValueMapperFactory testValueMapperFactory() {
        return (abstractTypes, environment) -> new ValueMapper() {
            @Override
            public <T> T fromInput(Object graphQLInput, Type sourceType, AnnotatedType outputType) {
                return null;
            }

            @Override
            public <T> T fromString(String json, AnnotatedType type) {
                return null;
            }

            @Override
            public String toString(Object output) {
                return null;
            }
        };
    }

NOTE: SPQR comes with JacksonValueMapper and GsonValueMapperFactory so in reality this should be rarely needed as these are by far the most frequently used libraries in Java.

Customizing input and output converters

Analogous to the rest of the configuration, single beans should be wired into the context. As this is done in functional style in SPQR it is not possible to set chains of InputConverter and OutputConverter, but by passing a lambda that will manipulate the chains.

Extension provider for input converters

    @Bean
    public ExtensionProvider<GeneratorConfiguration, InputConverter> testInputConverterExtensionProvider() {
        return (config, current) -> current.prepend( //Insert before the defaults. Or return a new list to take full control.
            new InputConverter() {
                @Override
                public Object convertInput(Object substitute, AnnotatedType type, GlobalEnvironment environment, ValueMapper valueMapper) {
                    return ...;
                }

                @Override
                public boolean supports(AnnotatedType type) {
                    return ...;
                }

                @Override
                public AnnotatedType getSubstituteType(AnnotatedType original) {
                    return ...;
                }
            }
        );
    }

Extension provider for output converters

    @Bean
    public ExtensionProvider<GeneratorConfiguration, OutputConverter> testOutputConverterExtensionProvider() {
         //Insert a custom converter after the built-in IdAdapter (which is generally a safe position).
         //Return a new list instead to take full control. 
        return (config, current) -> current.insertAfter(IdAdapter.class,
            new OutputConverter() {
                @Override
                public Object convertOutput(Object original, AnnotatedType type, ResolutionEnvironment resolutionEnvironment) {
                    return ...;
                }

                @Override
                public boolean supports(AnnotatedType type) {
                    return ...;
                }
            }
         );
    }

Custom type mapper for GraphQL output and input types

Again wire a single bean of type ExtensionProvider<GeneratorConfiguration, TypeMapper> into the application context to manipulate mapper chain.

    @Bean
    public ExtensionProvider<GeneratorConfiguration, TypeMapper> customTypeMappers() {
    //Insert a custom mapper after the built-in IdAdapter (which is generally a safe position)
    return (config, current) -> current.insertAfter(IdAdapter.class,
            new TypeMapper() {
                @Override
                public GraphQLOutputType toGraphQLType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
                    return new GraphQLOutputType() {
                        @Override
                        public String getName() {
                            return ...;
                        }
                    };
                }

                @Override
                public GraphQLInputType toGraphQLInputType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
                    return new GraphQLInputType() {
                        @Override
                        public String getName() {
                            return ...;
                        }
                    };
                }

                @Override
                public boolean supports(AnnotatedType type) {
                    return ...;
                }
            }
        );
    }

Custom argument injector

Also has a functional API, utilised by wiring a single bean of type ExtensionProvider<GeneratorConfiguration, ArgumentInjector>.

    @Bean
    public ExtensionProvider<GeneratorConfiguration, ArgumentInjector> testArgumentInjectorExtensionProvider() {
        return (config, current) -> current.prepend(
           new ArgumentInjector() {
               @Override
               public Object getArgumentValue(ArgumentInjectorParams params) {
                   return ...;
               }

               @Override
               public boolean supports(AnnotatedType type, Parameter parameter) {
                   return ...;
               }

           }
        );
    }

Custom input fields

Wiring a single bean of type ExtensionProvider<GraphQLSchemaGenerator.ExtendedConfiguration, InputFieldBuilder> will allow you to manipulate the input builder chain.

    @Bean
    public ExtensionProvider<ExtendedGeneratorConfiguration, InputFieldBuilder> testInputFieldBuilder() {
        return (config, current) -> current.prepend( //Prepend your custom builder so it goes before the built-in ones
                new InputFieldBuilder() {
                    @Override
                    public Set<InputField> getInputFields(InputFieldBuilderParams params) {
                        return ...; //Build the input fields for the given type
                    }

                    @Override
                    public boolean supports(AnnotatedType type) {
                        return ...; //Does this builder support the given type?
                    }
                });
    }

NOTE: In SPQR InputFieldBuilder is already implemented by JacksonValueMapper and GsonValueMapper.

More to follow soon ...

graphql-spqr-spring-boot-starter's People

Contributors

baldylocks avatar csueiras avatar ertugrulsener avatar esc-sbarden avatar icarus8050 avatar kaqqao avatar kicktipp avatar michael-simons avatar saschapeukert avatar vince250598 avatar vjroby avatar vkachan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-spqr-spring-boot-starter's Issues

JSON parse error: Cannot construct instance of `java.util.LinkedHashMap`

Hello, I am trying to use spring boot auto starter in my project. I am not sure what I am missing.
I am getting below error :

JSON parse error: Cannot construct instance of java.util.LinkedHashMap (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value (''); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of java.util.LinkedHashMap (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('')\n at [Source: (PushbackInputStream); line: 1, column: 70] (through reference chain: io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest["variables"])",
"path": "/graphql"

Below is my service class in spring boot application.

@GraphQLApi
@Service
public class Serviceq {

	@Autowired
	PRepository pRepo;

	@Transactional
	@GraphQLQuery(name = "party")
	public Test getPByNum(@GraphQLArgument(name = "num") String num)
					throws Exception, ExecutionException {

		Future<Test> test= partyRepo.findByNum(num);
		return test.get();
	}

}

I am requesting with GraphiQL.

{
  party (num: "1234"){
    id
    name
   number
  }
}

Using Spring data Pageable and Sort

Hi there,

I am struggling to get my API to work with org.springframework.data.domain.PageRequest as input to a graphql query resolver.

public Page<Discussion> getPagedDiscussions(@GraphQLArgument(name = "pagingInfo") PageRequest pageRequest) {}

When I load graphiql I get a message saying "SortInput fields must be an object with field names as keys or a function which returns such an object."

I tried custom InputFieldBuilders but it doesn't work :(

        inputFieldBuilders.add(new InputFieldBuilder() {
            @Override
            public Set<InputField> getInputFields(InputFieldBuilderParams params) {
                AnnotatedType type = params.getType();

                Set<InputField> fields = new HashSet<>();
                fields.add(new InputField("pageNumber", "Page number", GenericTypeReflector.annotate(Integer.class), GenericTypeReflector.annotate(Integer.class), 0));
                fields.add(new InputField("pageSize", "Page size", GenericTypeReflector.annotate(Integer.class), GenericTypeReflector.annotate(Integer.class), 20));
                fields.add(new InputField("sort", "Page sorting", GenericTypeReflector.annotate(Sort.class), GenericTypeReflector.annotate(Sort.class), null));
                return fields;
            }

            @Override
            public boolean supports(AnnotatedType type) {
                return PageRequest.class.equals(type.getType());
            }
        });


        inputFieldBuilders.add(new InputFieldBuilder() {
            @Override
            public Set<InputField> getInputFields(InputFieldBuilderParams params) {
                AnnotatedType type = params.getType();

                Set<InputField> fields = new HashSet<>();
                fields.add(new InputField("orders", "Orders for sorting", GenericTypeReflector.annotate(Sort.Order[].class), GenericTypeReflector.annotate(Sort.Order[].class), 20));
                return fields;
            }

            @Override
            public boolean supports(AnnotatedType type) {
                return Sort.class.equals(type.getType());
            }
        });
        inputFieldBuilders.add(new InputFieldBuilder() {
            @Override
            public Set<InputField> getInputFields(InputFieldBuilderParams params) {
                AnnotatedType type = params.getType();

                Set<InputField> fields = new HashSet<>();
                fields.add(new InputField("direction", "Sorting order direction", GenericTypeReflector.annotate(String.class), GenericTypeReflector.annotate(String.class), 20));
                fields.add(new InputField("property", "Sorting order property", GenericTypeReflector.annotate(String.class), GenericTypeReflector.annotate(String.class), 20));
                return fields;
            }

            @Override
            public boolean supports(AnnotatedType type) {
                return Sort.Order.class.equals(type.getType());
            }
        });

TypeMappingException: Type io.leangen.graphql.spqr.spring.autoconfigure.DefaultGlobalContext is unbounded or missing generic type parameters

Hi I believe I found a bug, whenever the DefaultGlobalContext is injected without the type being specified for the request object a TypeMappingException is raised:

Caused by: io.leangen.graphql.metadata.exceptions.TypeMappingException: Type io.leangen.graphql.spqr.spring.autoconfigure.DefaultGlobalContext is unbounded or missing generic type parameters
	at io.leangen.graphql.util.ClassUtils.completeGenerics(ClassUtils.java:509) ~[spqr-0.9.9.jar:na]
	at io.leangen.graphql.metadata.strategy.type.DefaultTypeTransformer.transform(DefaultTypeTransformer.java:28) ~[spqr-0.9.9.jar:na]
	at io.leangen.graphql.metadata.strategy.query.AnnotatedArgumentBuilder.buildResolverArguments(AnnotatedArgumentBuilder.java:40) ~[spqr-0.9.9.jar:na]
	... 65 common frames omitted

Here's an example to reproduce:

@GraphQLApi
@Service
public class SpqrBugService {
    @GraphQLNonNull
    @GraphQLQuery(name = "coolPeople")
    public List<String> findCoolPeople(@GraphQLArgument(name = "coolnessFactor") final int factor,
                                       @GraphQLRootContext final DefaultGlobalContext ctx) {

        if (factor > 5) {
            return ImmutableList.of("Me");
        }

        return ImmutableList.of();
    }
}

A workaround is to do:

@GraphQLRootContext final DefaultGlobalContext<Object> ctx

graphql-spqr version 0.9.9
graphql-spqr-spring-boot-starter 0.0.4

GraphQLSubscription "InvalidDefinitionException" error

Hi,

I'm newbie to spring reactive so this question can be silly, sorry for that.
I'm trying to use graphqlsubscription from looking to the example.
Here is my code:

`private final ConcurrentMultiMap<String, FluxSink> subscribers = new ConcurrentMultiMap<>();

@GraphQLSubscription
public Publisher expUpdated(String code) {
return Flux.create(subscriber -> subscribers.add(code, subscriber.onDispose(() -> subscribers.remove(code, subscriber))), FluxSink.OverflowStrategy.LATEST);
}`

And here is the error:

org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class graphql.execution.reactive.CompletionStageMappingPublisher]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class graphql.execution.reactive.CompletionStageMappingPublisher and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.LinkedHashMap["data"])
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:293) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]

ClassCastException when class implements multiple Union-Interfaces and is used as return type

I have a UserDto.class which implements two interfaces which both are used as union-interfaces (to handle errors in the response instead of throwing an exception).

public class UserDto implements IUserResult, IFindUserResult { // fields/getter/setter }
@GraphQLUnion(possibleTypes = {UserDto.class, UserRegistrationErrorDto.class})
public interface IUserResult {}
@GraphQLUnion(possibleTypes = {UserDto.class, UserNotFoundDto.class})
public interface IFindUserResult {}

In the service class i try to return the UserDto object but get the following error:
java.util.concurrent.CompletionException: java.lang.ClassCastException: graphql.schema.GraphQLTypeReference cannot be cast to graphql.schema.GraphQLObjectType

Method in service class:
Returns interface type. Returning a UserRegistrationError object works, but returning the UserDto object throws the above mentioned error. :

    @GraphQLMutation
    public IUserResult registerUserAccount(
            @GraphQLArgument(name = "user") UserRegistrationInputDto user) {

        // validation omitted

        List<ErrorDto> errors = new ArrayList<>();
        //check duplicate email addresses
        if(userRepository.findByEmail(user.getEmail()).isPresent()){
            errors.add(new ErrorDto("email", "user already registered with this email address"));
            return new UserRegistrationErrorDto(errors);
        }

        User toRegister = modelMapper.map(user, User.class);
        User registered = userRepository.save(toRegister);
        return modelMapper.map(registered, UserDto.class);
    }

This error does not occur when UserDto.class only implements one interface, but this would break the ability to return error types/messages without throwing exceptions.
See example project at https://github.com/ssiemens-hm/spgr-graphql-test

Used GraphQL-Mutation:

mutation {
  registerUserAccount(
    user: {
      firstName: "firstname"
      lastName: "lastname"
      email: "[email protected]"
      password: "password"
      matchingPassword: "password"
    }
  ) {
    __typename
    ... on User {
      id
      firstName
      lastName
    }
    ... on UserRegistrationError {
      errors {
        field
        reason
      }
    }
  }
}

Support for Spring Security (needs CSRF Token for graphiql)

When adding spring-boot-starter-security you have to disable CSRF protection, because the graphiql user interface doesn't send csrf token when doing requests to POST /graphql.

Don't know if that's possible at all. Else just close this issue.

[Autoconfigure] First impressions

Hi there!

Congratulations on this amazing GraphQL spring boot extension, it was really easy to setup our current spring boot project to use this graphql-spqr-spring-boot-autoconfigure dependency.

To help this project get even better, I would like to suggest a few improvements:

  • Remove the SPQR banner. This is a dependency, so it should not add a banner to the project.
  • Document the spring-boot-starter-websocket dependency. Without this the application crashes after booting up.

Best regards,
Rafael Renan Pacheco.

Support Spring Data paging out of the box

Firstly, we must make sure Spring Data paging types (Page, Pageable) map nicely out of the box.

Additionally, we should have custom mappers, and whatever else necessary, to optionally automatically map Spring paging style to Relay Connection style.

A similar issue was raised in SPQR (leangen/graphql-spqr#127).

Data loader in subscriptions

Hi!

I'm trying to use subscriptions with this library, but unfortunately it seems that data loaders are not working with subscriptions. The problem seems to be that the code in ApolloProtocolHandler doesn't pass the dataLoaderRegistry to the ExecutionInput.Builder.

ApolloProtocolHandler line 91:

ExecutionResult result = graphQL.execute(ExecutionInput.newExecutionInput()
                            .query(request.getQuery())
                            .operationName(request.getOperationName())
                            .variables(request.getVariables()));

The missing lines are (copied from GraphQLController:

        if (dataLoaderRegistryFactory != null) {
            inputBuilder.dataLoaderRegistry(dataLoaderRegistryFactory.createDataLoaderRegistry());
        }

@kaqqao can you take a look at it?

Thanks,
Gabor

How to set current user in the context?

I couldn't figure out how to store currently authenticated user in the context so that I can use it for authorisation for example. And does this library provide any easy way to do authorisation?

Deal with generated proxies

Currently, if a bean is wrapped into a proxy for any reason (security interceptors, async support, transactional control etc), SPQR will reject the bean as it's package name can no be reliably determined.

The starter should explicitly specify the real types when registering the discovered beans.

Support for configuring GraphQL object

Currently the DefaultGraphQLController autowires a GraphQLSchema, and builds a default GraphQL object. This doesn't leave room to configure an ExecutionStrategy or Instrumentation. The DefaultGraphQLController could instead autowire a GraphQL, which can be overridden for custom configuration.

Customize ObjectMapper in ApolloMessage

Hi, awesome library, thanks for it!

Is it planned to be able to customize ObjectMapper in the ApolloMessage class?

My use case is that I'd like to serialize LocalDateTime as a string (by default it is serialized as an Object, it's really weird).

mapper field in ApolloMessage Class

Hello,

In ApolloMessage the object mapper does not include null.

private static final ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);

When null value are not included apollo-client is warning about missing fields in queries causing some weird behaviours when updatingQueries.

Is there a good reason to configure the mapper like that?

Support to add custom url than the static /graphql

So I look around and played around a bit and I think letting users customize/have their own url endpoints is also a great addition to this awsome library.

I can push to this repo but if you can just take out the /graphql in the GrpahQLController class and then specify the property in your application.properties file. Sample below

change below

@PostMapping(
            value = "${graphql.spqr.http.endpoint:/graphql}",
            consumes = {"application/graphql", "application/graphql;charset=UTF-8"},
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE
    )

to

@PostMapping(
            value = "${graphql.spqr.http.endpoint}",
            consumes = {"application/graphql", "application/graphql;charset=UTF-8"},
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE
    )

and then place this graphql.spqr.http.endpoint=/customUrl in your projects application.properties file

Spring Security Context + Subfields

Hey @kaqqao I've pretty successfully integrated with spring security and SPQR but I just noticed a bug in my code that manifests itself only with subfields that are making use of @GraphQLContext, and more specifically so far I've discovered this in a Subscription. For this example my subscription is returning the Stuff objects.

Basically I have something like this:

@PreAuthorize("isAuthenticated()")
@GraphQLQuery
public Stuff getStuff() {
  return repository.getStuff();
}

@PreAuthorize("isAuthenticated()")
@GraphQLQuery
public MoreStuff getMoreStuff(@GraphQLContext Stuff stuff) {
   final MyAuthentication auth = (MyAuthentication)SecurityContextHolder.getContext().getAuthentication();
   return repository.getMoreStuff(stuff.getStuffId(), auth.getUserId());
}

This results in a couple of issues, first the PreAuthorize on the getMoreStuff query field cannot find an authenticated user in its context. As you may know SecurityContextHolder uses a ThreadLocal variable to keep track of the current security context, and I know that getMoreStuff and getStuff are executed in different threads, so this explains that issue. I tried to enable inheritable thread local strategy on the security context holder but that wouldnt work either because my getStuff thread is not the one initiating this other thread.

I assume you or someone else in the community has probably solved this issue before and I was wondering if I could get some insight on how its been solved for GraphQL. I prototyped something that while would work its also a lot of work...

Basically any parent field would have to do:

@PreAuthorize("isAuthenticated()")
@GraphQLQuery
public Stuff getStuff(@GraphQLRootContext DefaultGlobalContext<HttpServletRequest> context) {
  // Store the authenticated user in the GraphQL DefaultGlobalContext so that it can be retrieved by sub fields
  ContextUtils.saveAuthenticatedUser(context, authenticatedUser);
  return repository.getStuff();
}

Then in my subfield I can do:

@GraphQLQuery
public MoreStuff getMoreStuff(@GraphQLRootContext DefaultGlobalContext<HttpServletRequest> context, @GraphQLContext Stuff stuff) {
   final MyAuthentication auth = ContextUtils.getAuthenticatedUser(context);
   return repository.getMoreStuff(stuff.getStuffId(), auth.getUserId());
}

As you can imagine this is very error prone and most likely I'm bruteforcing something in a way that can probably be done a lot easier. I was thinking of maybe an argument injector could be the way, basically create an argument injector that propagates the security context to subfields? I haven't found an example of something like this just yet but perhaps you have some pointers.

Thanks

Custom Exception Handling with Spring Method Level Security

Hi,

thanks for this library. I'm using method level security to restrict the graphql endpoints. So far this works great. However, if a user lacks authentication the application returns an AccessDeniedException (as expected) which I can't seem to catch.

I'd greatly appreciate any advice.

Example code:

@GraphQLApi
@Component
public class UserGraphQLService {

    @PreAuthorize("hasRole('ROLE_USER')")
    @GraphQLQuery
    public String someMethod() {
        //...
    }

}

Results in:

org.springframework.security.access.AccessDeniedException: Access Denied
	at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65) ~[spring-security-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at my.application.UserGraphQLService$$EnhancerBySpringCGLIB$$a4cbe9c.someMethod(<generated>) ~[main/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at io.leangen.graphql.metadata.execution.SingletonMethodInvoker.execute(SingletonMethodInvoker.java:21) ~[spqr-0.9.9.jar:na]
	at io.leangen.graphql.metadata.Resolver.resolve(Resolver.java:100) ~[spqr-0.9.9.jar:na]
	at io.leangen.graphql.execution.OperationExecutor.lambda$execute$1(OperationExecutor.java:93) ~[spqr-0.9.9.jar:na]
	at io.leangen.graphql.execution.OperationExecutor.execute(OperationExecutor.java:98) ~[spqr-0.9.9.jar:na]
	at io.leangen.graphql.execution.OperationExecutor.execute(OperationExecutor.java:94) ~[spqr-0.9.9.jar:na]
	at io.leangen.graphql.execution.OperationExecutor.execute(OperationExecutor.java:59) ~[spqr-0.9.9.jar:na]
	at graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation.lambda$instrumentDataFetcher$0(DataLoaderDispatcherInstrumentation.java:86) ~[graphql-java-11.0.jar:na]
	at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:261) ~[graphql-java-11.0.jar:na]
	at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:202) ~[graphql-java-11.0.jar:na]
	at graphql.execution.ExecutionStrategy.resolveField(ExecutionStrategy.java:175) ~[graphql-java-11.0.jar:na]
	at graphql.execution.AsyncSerialExecutionStrategy.lambda$execute$1(AsyncSerialExecutionStrategy.java:43) ~[graphql-java-11.0.jar:na]
	at graphql.execution.Async.eachSequentiallyImpl(Async.java:75) ~[graphql-java-11.0.jar:na]
	at graphql.execution.Async.eachSequentially(Async.java:64) ~[graphql-java-11.0.jar:na]
	at graphql.execution.AsyncSerialExecutionStrategy.execute(AsyncSerialExecutionStrategy.java:38) ~[graphql-java-11.0.jar:na]
	at graphql.execution.Execution.executeOperation(Execution.java:159) ~[graphql-java-11.0.jar:na]
	at graphql.execution.Execution.execute(Execution.java:101) ~[graphql-java-11.0.jar:na]
	at graphql.GraphQL.execute(GraphQL.java:573) ~[graphql-java-11.0.jar:na]
	at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:515) ~[graphql-java-11.0.jar:na]
	at graphql.GraphQL.executeAsync(GraphQL.java:489) ~[graphql-java-11.0.jar:na]
	at graphql.GraphQL.execute(GraphQL.java:420) ~[graphql-java-11.0.jar:na]
	at io.leangen.graphql.spqr.spring.web.servlet.DefaultGraphQLExecutor.execute(DefaultGraphQLExecutor.java:23) ~[graphql-spqr-spring-boot-autoconfigure-0.0.4.jar:na]
	at io.leangen.graphql.spqr.spring.web.servlet.DefaultGraphQLExecutor.execute(DefaultGraphQLExecutor.java:11) ~[graphql-spqr-spring-boot-autoconfigure-0.0.4.jar:na]
	at io.leangen.graphql.spqr.spring.web.GraphQLController.executeJsonPost(GraphQLController.java:42) ~[graphql-spqr-spring-boot-autoconfigure-0.0.4.jar:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:320) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:119) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:170) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:100) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:74) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) ~[spring-security-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:836) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1747) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
	at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

Enable easy customization of GraphQL execution

In order to customize any of the following:

  • Building ExecutionInput
  • Executing the operation
  • Handling the response

it is currently necessary to override the whole controller and/or WebSocket handler.

To facilitate easier customization, all of these concerns are now delegated to GraphQLExecutor implementation that can easily be swapped by the user.

Right now, 3 built-in implementations exist (with at least 1 more to come):

  • servlet.DefaultGraphQLExecutor - used by the GraphQL controller in a servlet-based app
  • reactive.DefaultGraphQLExecutor - used by the GraphQL controller in a reactive app
  • servlet.websocket.DefaultGraphQLExecutor - used by the WebSocket handler in a servlet-based app

Once WebSockets are supported on the reactive stack, an additional executor will be added.

Use String instead of Enum names in GraphQL queries

Hi, this is kind of the same issue raised here in graphql-spqr, but the solution provided by kaqqao in that issue seems not available with graphql-spqr-spring-boot-starter. Basically kaqqao suggested to implement a custom mapper and register it when generating schema. However while using graphql-spqr-spring-boot-starter, schema is automatically generated and configured. So can we have this feature supported in this spring-boot-starter in the future?

To make the issue clear, suppose I have a Pet class with PetType enum:

public class Pet implements Serializable {
    private String name;
    private PetType type;
}

public enum PetType implements Serializable {
    PUPPY("Dog"),
    KITTY("Cat");

    private final String value;

    PetType(String value) {
        this.value = value;
    }
}

and I have a PetStoreService:

@GraphqlApi
public class PetStoreService {
    @AutoWired
    private PetService petService;

    @GraphQLQuery(name = "getPetsByType")
    public List<Pet> getPetsByType(@GraphqlQLArgument(name = "petType") PetType type) {
        return petService.getPetsByType(type);
    }
}

Now I want my customers to fire GraphQL queries like this:

query {
    getPetsByType(petType: "Dog") {
        name
        type
    }
}

that is, use "Dog" or "Cat" rather than PUPPY or KITTY in queries. Kaqqao mentioned another solution in the previous issue, which is to use @GraphQLEnumValue(name = "Dog") PUPPY. However this annotation just gives each enum type an alias, not a String value. Customers still have to fire queries like this:

query {
    getPetsByType(petType: Dog) {
        name
        type
    }
}

Which is not what I expected. How can I achieve this with graphql-spqr-spring-boot-starter? Thanks a lot!

Connection error after receiving GQL_CONNECTION_INIT

When the client sends a GQL_CONNECTION_INIT (connection_init) message to the server, the server now expects (Jackson) an id field to be present in the message. However, the apollo client doesn't seem to send the id field in this message, so the server responds with a GQL_CONNECTION_ERROR (connection_error) response due to Jackson being unable to parse the message.

The problem seems to be in ApolloProtocolHandler:79 :

 try {
            ApolloMessage apolloMessage;
            try {
                apolloMessage = ApolloMessage.from(message);
            } catch (IOException e) {
                session.sendMessage(ApolloMessage.connectionError());
                return;
            }

Now the question is, is Apollo not following the GraphQL WS specification, or the server is to blame here?

Using starter on application having @EnableScheduling cause an exception

In an application with @EnableScheduling annotation, I'm facing the following error:

org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'defaultSockJsTaskScheduler' is expected to be of type 'org.springframework.scheduling.TaskScheduler' but was actually of type 'org.springframework.beans.factory.support.NullBean'
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:392)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:224)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1115)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1082)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.resolveSchedulerBean(ScheduledAnnotationBeanPostProcessor.java:313)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.finishRegistration(ScheduledAnnotationBeanPostProcessor.java:254)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:231)
	at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.onApplicationEvent(ScheduledAnnotationBeanPostProcessor.java:103)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:402)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:359)
	at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:896)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:163)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:552)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)

Apollo subscriptions on the reactive stack

We already support Spring WebFlux for queries and mutations (#9), but Apollo graphql-ws only works with Spring MVC.
We need a reactive WebSocket implementation to complete the WebFlux support.

how to set basePackages

i want to use GraphQLInterface, but throw error 'not implement interface...'.
i search graphql-spqr, set basePackages will resolve this problem.
but i do not know how to set basePackages in this case.

Subscriptions not working with withOperationsFromSingleton

Perhaps not an issue - I'm a bit of a newbie so forgive my ignorance :)

I'm using the new built in subscriptions with Apollo (@GraphQLSubscription). It all works beautifully, unless I add .withOperationsFromSingleton() to the GraphQLSchemaGenerator. I had originally thought the subscriptions stuff was not working at all but since realised it works without that config

Any pointers? Thanks

Annotation to enable SPQR and set packages

Usually, in Spring Boot starters there's a @EnableXyz annotation you use to enable the autoconfiguration and set the packages to scan. It would be useful for this starter to conform to this convention! For example:

@SpringApplication
@EnableSpqr("my.package")
public class MyApplication {}

This also let one enable SPQR in its own starters, without the need to specify the packages in a configuration file.

Websockets with GraphQL Playground

I am using 0.0.2 version. If I enable websocket support via graphql.spqr.ws.enabled=true then websockets doesn't work with GraphQL Playground. It appears that SockJsHttpRequestHandler is being used by the app instead of PerConnectionWebSocketHandler. See below line from application startup logs -

Mapped URL path [/graphql/**] onto handler of type [class org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler]

Can anyone help with this? I have been using SPQR to build an app from last 3 months but because of Websocket support issues my development work is not crippled. :( I don't want to write a separate NodeJs app with Apollo server.

how the directive of the client works?

@GraphQLDirective(name = "timeout", locations = Introspection.DirectiveLocation.FIELD)
public static class Interrupt {
    @GraphQLInputField(name = "afterMillis") //Customize the fields as usual
    public int after;
}

{
    books(searchString: "Monkey") @timeout(afterMillis: 500) {
        title
    }
}

throw err:
Validation error of type UnknownDirective: Unknown directive timeout

Apollo websocket authentication with Spring Security

I can't authenticate over websocket using Apollo aproach:

const wsLink = new WebSocketLink({
uri: 'ws://localhost:9090/graphql',
  options: {
    reconnect: true,
    connectionParams: {
        authToken: user.authToken,
    },
});

The response is "HTTP Authentication failed; no valid credentials available"

How I can manage the connection params 'authToken' in Spring?

variables passed to the ExecutionInput cannot be null

When the variables parameter is not being passed as part of the request, graphql-java throws the following exception. This, in particular, is a nightly version of graphql-java which makes the issue explicit (graphql-java/graphql-java#1596). Earlier versions of graphql-java (from 11 on) fail with more obscure errors.

Would it make sense to ensure the propagated variables instance is never null and rather an empty map?

graphql.AssertException: variables map can't be null
	at graphql.Assert.assertNotNull(Assert.java:15)
	at graphql.ExecutionInput$Builder.variables(ExecutionInput.java:215)
	at io.leangen.graphql.spqr.spring.web.GraphQLExecutor.buildInput(GraphQLExecutor.java:20)
	at io.leangen.graphql.spqr.spring.web.servlet.DefaultGraphQLExecutor.execute(DefaultGraphQLExecutor.java:23)
	at io.leangen.graphql.spqr.spring.web.servlet.DefaultGraphQLExecutor.execute(DefaultGraphQLExecutor.java:11)
	at io.leangen.graphql.spqr.spring.web.GraphQLController.executeJsonPost(GraphQLController.java:42)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)

The above issue, in particular, is triggered simply loading a GraphiQL page which attempts an introspection query that doesn't include a variables field in the request.

Edit prefix Input object

Dear bro,
I have issue with auto generator input type such as:
I have class CreateCompanyInput
And SPQR auto generator CreateCompanyInputInput. How can i fix it?

Start Project using Spring boot

When I import dependencies, my project not finding annotations, like @GraphQLAp, how I setup it?

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!--Will be optional as of 0.0.2-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>
  <dependency>
    <groupId>io.leangen.graphql</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>0.0.1</version>
  </dependency>
  <!--Will be implicit as of 0.0.2-->
  <dependency>
    <groupId>io.leangen.graphql</groupId>
    <artifactId>spqr</artifactId>
    <version>0.9.7</version>
  </dependency>
</dependencies>

get Authentication with WebFlux

I have a need that get authentication inside @GraphQLMutation method, this might be a common need, as was mentioned in #11, However when using with WebFlux, the solution mentioned there is out of style.

SecurityContextHolder.getContext().getAuthentication() will return null in WebFlux, as explained in this StackOverflow post

Because there is no way to use ThreadLocal objects anymore. The only way to get Authentication for you, is to ask for it in controller's method signature, or...
Return a reactive-chain from method, that is making a ReactiveSecurityContextHolder.getContext() call.

In the case of this project, however, I cannot modify the Controller method, or return the reactive-chain that is making ReactiveSecurityContextHolder.getContext() call.

I found a workaround to return the reactive-chain, by providing a custom GraphQLReactiveExecutor, like this:

@Component
public class DefaultGraphQLExecutor implements GraphQLReactiveExecutor {

    @Autowired(required = false)
    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
    private DataLoaderRegistryFactory dataLoaderRegistryFactory;


    @Override
    public Mono<Map<String, Object>> execute(GraphQL graphQL, GraphQLRequest graphQLRequest, ServerWebExchange request) {
        return ReactiveSecurityContextHolder.getContext()
                .flatMap(securityContext -> Mono.fromFuture(
                        graphQL.executeAsync(
                                buildInput(
                                        graphQLRequest,
                                        request,
                                        (params -> {
                                            DefaultGlobalContext<ServerWebExchange> context = new DefaultGlobalContext<>(params.getNativeRequest());
                                            context.setExtension("authentication", securityContext);
                                            return context;
                                        }),
                                        dataLoaderRegistryFactory)
                        ).thenApply(ExecutionResult::toSpecification)));
    }
}

Then I will be able to get authentication information inside @GraphQLMutation method by injecting:@GraphQLRootContext DefaultGlobalContext<SecurityContextServerWebExchange> context and calling context.getExtension("authentication").getAuthentication(),

    @Transactional
    @GraphQLMutation
    public Authentication getAuthentication(
            @GraphQLRootContext DefaultGlobalContext<SecurityContextServerWebExchange> context) {
        SecurityContext securityContext = context.getExtension("authentication");
        return securityContext.getAuthentication();
    }

Is there any better way to achieve that?

Throw exception on type clash

I just created two @Services with clashing method names:

@Service
@GraphQLApi
public class MyService1 {
  @GraphQLMutation
  public String foo() {
    return "1";
  }
}

@Service
@GraphQLApi
public class MyService2 {
  @GraphQLMutation
  public String foo() {
    return "2";
  }
}

Now it's completely random (changes every time I restart the application) which of those 2 methods gets called.

I think it would be best, if the auto configuration throws an exception, when adding a GraphQL Type that already exists.

fail to GET request with variables

reported at #52 (comment)

Currently the transmission of variables in the request via puery param does not work either. When I send a request (as defined in the docu https://graphql.org/learn/serving-over-http/), the following error occurs

Field error in object 'graphQLRequest' on field 'variables': rejected value [{
"$content": "123"
}]; codes [typeMismatch.graphQLRequest.variables,typeMismatch.variables,typeMismatch.java.util.Map,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [graphQLRequest.variables,variables]; arguments []; default message [variables]]; default message [Failed to convert value of type 'java.lang.String[]' to required type 'java.util.Map'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String[]' to required type 'java.util.Map': no matching editors or conversion strategy found]]

	@Test
	public void defaultControllerTest_GET_with_variables() throws Exception {
		mockMvc.perform(
				get("/"+apiContext)
						.param("query","query echo($content: String) {echo(content: $content)}")
						.param("variables", "{\n" +
                                "  \"$content\": \"123\"\n" +
                                "}")
						)
				.andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello world")));
	}

reason: Jackson databiner doesn't know how to convert String to Map.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.