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

Generate FieldValidationInstrumentation

Open aSemy opened this issue 7 years ago • 13 comments
trafficstars

graphql-java has recently implemented field level validation that is tested after the query is parsed, but before it is executed. https://github.com/graphql-java/graphql-java/issues/162

It would be really handy if this could be automatically generated using javax.validation annotations!

I'm only just starting my own implementation so I haven't thought it through fully, but I can see it working as follows:

  1. In the method that generates a GraphQLSchema, also generate a FieldValidationInstrumentation
  2. This generate would, for each GraphQL query or mutation, check the arguments
  3. For each argument and fields of each argument get any javax.validation annotations
  4. Create a rule for each annotation. Add each rule to a SimpleFieldValidation
  5. The programmer could then add the FieldValidationInstrumentation to the GraphQL builder GraphQL.newGraphQL(...).instrumentation(instrumentation).build()

It would also be nice to have a custom org.springframework.validation.Validator bean that could check business logic (e.g. the email already exists in the database) but I haven't thought about how that could be implemented yet.

There's an example of the graphql-java validation in the method fieldValidation(), Line 161, shows an example.

https://github.com/graphql-java/graphql-java/blob/2f8f3de1d722ec0128b960c5e565405057fa76ff/src/test/groovy/readme/InstrumentationExamples.java#L161

aSemy avatar Jan 26 '18 10:01 aSemy

This is an interesting proposition, and a framework generating a schema is indeed in a perfect position to attach such a validation, but I have to think about it more thoroughly. It entails dragging in a dependency to a bean validation implementation, and extending the generator to generate more than the schema...

As for a Spring Validator, won't it already work just fine if the beans you expose with SPQR are managed beans?

kaqqao avatar Jan 26 '18 10:01 kaqqao

Oh, and since you mentioned you're starting your own implementation, if it ends up being something you can contribute - please do ^_^

kaqqao avatar Jan 26 '18 10:01 kaqqao

Thanks for the consideration!

javax.validation is included in the Spring Starter, so I assumed that would be fair game to include, though of course unnecessary dependencies are bad.

I'm looking into this again today, maybe you can help. So as I understand, basically, SPQR fetches all Java methods, method arguments, and class fields that are annotated. I think you could just loop over every thing, and for each validation annotation create a new rule.

I can't see how to get all the mutations, queries, arguments though. How can I do this?

for (Method mutation : @GraphQLMutations) {
    for (Argument argument : @GraphQLArguments) {
        // somehow fetch validation annotations like @NotNull?
        for (Annotation a : validationAnnotations) {
            rules.add(
                new BiFunction<...>(){
                    @Override
                    public Optional<GraphQLError> apply(FieldAndArguments fieldAndArguments, FieldValidationEnvironment env) {
                        Object o = fieldAndArguments.getArgumentValue(mutation.getName()).get(argument.getName());
                        if (a.isValid(o)) 
                            return Optional.empty();
                        else
                            return Optional.of(environment.mkError("error on " + argument.getName(), fieldAndArguments));
                    }
                }
            )
        }
    }
}

aSemy avatar Jan 30 '18 09:01 aSemy

javax.validation is already an optional dependency (you can use it's @Nonnull to produce a GraphQLNonnullType), so that one is ok. But I wouldn't want to actually implement the validation logic mandated by JSR 380 myself. I'd instead delegate it to an existing implementation (Hibernate Validator, in all likelihood). I'd like to put this into a separate module that would be auto-detected, similar to how I'm planning to make library specific mappers (e.g. Joda Time mappers, Spring Flux mappers etc) work. But, it may ultimately makes more sense to add this is a module to the SPQR Spring Starter (being developed as we speak ;) ) instead of SPQR itself... As you can see, I have to give this some serious thought first. But it will surely materialize in some form 😄

As for the code above, right now there's no convenient way to get a hold of the raw java.reflect objects from the outside... I'd have to somehow expose them to the client code code via new methods/hooks on the generator.

kaqqao avatar Jan 30 '18 15:01 kaqqao

Okay, cool. Sounds like you've got a better idea of how to implement it than me ;)

I've put something together manually for now, without using annotations.

aSemy avatar Jan 31 '18 08:01 aSemy

+1 for this enhancement :)

phillip-kruger avatar Mar 15 '18 14:03 phillip-kruger

+1 for this enhancement :)

EdgarArguelles avatar Mar 19 '19 19:03 EdgarArguelles

@EdgarArguelles While this is something I'm still planning to look into, I think it's much less needed now than at the time this issue was opened. The reason is that these days it should be rather easy to use ResolverInterceptor to achieve the same result (check the tests for inspiration). Of course, it has always been possible to utilize the capabilities of the underlying framework (e.g. Spring, CDI, Guice etc) as most of them provide Bean Validation integration.

kaqqao avatar Mar 19 '19 20:03 kaqqao

Hey there! I am using spqr in a spring-boot environment. For authentication I need to read a JWT token from every incoming request. I found out how to do for a single method it with the @GraphQLRootContext annotation, but I want to do it on a global level. To my understanding this can be done with a ResolverInterceptor, but I don't know how to register it with spring. Can anyone help me?

Matthias-Walter-Innio avatar Jul 30 '19 12:07 Matthias-Walter-Innio

@Matthias-Walter-Innio I'd whole-heartedly suggest you use existing Spring features for handling security, as it already has everything you need to process JWT. Since SPQR will invoke Spring-managed beans, all Spring features will work normally.

If that's for some reason not an option, yes, ResolverInterceptor is the way to go, as it can intercept any resolver invocation and inspect the root context.

To register any extension in Spring, simply register a bean such as:

//E is any extension type, such as TypeMapper ResolverInterceptorFactory, or anything else
@Bean
public ExtensionProvider<GeneratorConfiguration, E> customExtensions() {
    return (config, extensions) -> extensions.append/drop/insert(...); //modify the current list as you please
}

For your case, if you want a ResolverInterceptor that is applicable globally (to all resolvers), you can use GlobalResolverInterceptorFactory:

@Bean
public ExtensionProvider<GeneratorConfiguration, ResolverInterceptorFactory> customInterceptors() {
    return (config, interceptors) -> interceptors.append(new GlobalResolverInterceptorFactory(customGlobalInterceptors);
}

If you want to react to e.g. an annotation (so that only annotated methods are intercepted), you can provide a custom ResolverInterceptorFactory. that way you e.g. only intercept top-level methods.

Inside of your ResolverInterceptor if the token in missing or invalid, you want to throw an AbortExecutionException to prevent further execution.

kaqqao avatar Jul 30 '19 13:07 kaqqao

Thanks a lot, I will try out!

Matthias-Walter-Innio avatar Jul 30 '19 14:07 Matthias-Walter-Innio

@Matthias-Walter-Innio Answered your SO question as well.

kaqqao avatar Jul 31 '19 12:07 kaqqao

Sorry if this is an old thread...I'm using JavaX validation constraints in my Spring app, how should I get SPQR to pick these up? Currently I have the below Object and am trying to validate this in my Graph query:

public class Example {
    @NotBlank
    @Pattern(regexp = ".{3,}")
    private String query;
}
  @GraphQLQuery(name = "exampleSearch")
  public List<String> searchList(@GraphQLArgument(name = "query") @RequestBody @Valid Example query) {
    return searchService.exampleSearch(query);
  }

kdotzenrod517 avatar Sep 17 '20 18:09 kdotzenrod517