如何将自定义指令添加到通过单例解决的查询中

时间:2018-07-23 19:32:49

标签: java graphql-java graphql-spqr

我设法将自定义指令添加到GraphQL模式,但是我在努力研究如何向字段定义中添加自定义指令。关于正确实现的任何提示将非常有帮助。 我正在使用GraphQL SPQR 0.9.6生成我的模式

1 个答案:

答案 0 :(得分:1)

目前无法执行此操作。 GraphQL SPQR v0.9.9将首先支持自定义指令。

仍然,在0.9.8中,有一个可能的解决方法,具体取决于您要实现的目标。 SPQR自己有关字段或类型的元数据保存在自定义指令中。知道这一点,您可以掌握GraphQL字段定义下的Java方法/字段。如果您想要的是一个基于指令执行某些操作的工具,您可以取而代之获得底层元素的任何注释,从而充分利用Java的威力。

获取方法的方式类似于:

Operation operation = Directives.getMappedOperation(env.getField()).get();
Resolver resolver = operation.getApplicableResolver(env.getArguments().keySet());
Member underlyingElement = resolver.getExecutable().getDelegate();

更新: 我在this GitHub issue上发布了一个巨大的答案。也将其粘贴到这里。

您可以这样注册其他指令:

generator.withSchemaProcessors(
    (schemaBuilder, buildContext) -> schemaBuilder.additionalDirective(...));

但是(根据我目前的理解),这仅对查询指令有意义(客户端作为查询的一部分发送的内容,例如@skip@deffered)。

诸如@dateFormat之类的指令在SPQR中根本毫无意义:它们在解析SDL并将其映射到您的代码时可以为您提供帮助。在SPQR中,没有SDL,您可以从代码开始。 例如。 @dateFormat用于告诉您在将其映射到Java时需要为特定字段提供日期格式。在SPQR中,您从Java部分开始,并且GraphQL字段是从Java方法生成的,因此该方法必须已经知道它应返回的格式。或者它已经具有适当的注释。 在SPQR中,Java是真理的源泉。您可以使用注释来提供额外的映射信息。指令基本上是SDL中的注释。

静态,字段或类型级别的指令(或注释)在检测中非常有用。例如。如果要拦截字段解析并检查身份验证指令。 在这种情况下,建议您仅出于相同目的使用注释。

public class BookService {

      @Auth(roles= {"Admin"}) //example custom annotation
      public Book addBook(Book book) { /*insert a Book into the DB */ }
}

由于每个GraphQLFieldDefinition都由Java方法(或字段)支持,因此您可以在拦截器中或任何地方获取基础对象:

GraphQLFieldDefinition field = ...;
Operation operation = Directives.getMappedOperation(field).get();

//Multiple methods can be hooked up to a single GraphQL operation. This gets the @Auth annotations from all of them
Set<Auth> allAuthAnnotations = operation.getResolvers().stream()
                .map(res -> res.getExecutable().getDelegate()) //get the underlying method
                .filter(method -> method.isAnnotationPresent(Auth.class))
                .map(method -> method.getAnnotation(Auth.class))
                .collect(Collectors.toSet());

或者,仅检查可以处理当前请求的方法:

DataFetchingEnvironment env = ...; //get it from the instrumentation params      
Auth auth = operation.getApplicableResolver(env.getArguments().keySet()).getExecutable().getDelegate().getAnnotation(Auth.class);

然后,您可以根据需要检查注释,例如

Set<String> allNeededRoles = allAuthAnnotations.stream()
                                             .flatMap(auth -> Arrays.stream(auth.roles))
                                             .collect(Collectors.toSet());

if (!currentUser.getRoles().containsAll(allNeededRoles)) {
    throw new AccessDeniedException(); //or whatever is appropriate
}

当然,实际上并不需要真正实现身份验证,因为您可能正在使用像Spring或Guice这样的框架(甚至Jersey都具有所需的安全性功能),该框架已经可以拦截所有方法,并且实施安全性。因此,您可以只使用它。更简单,更安全。例如。对于Spring Security,只需继续正常使用即可:

public class BookService {

      @PreAuth(...) //standard Spring Security
      public Book addBook(Book book) { /*insert a Book into the DB */ }
}

请确保您也正在阅读my answer on implementing security in GraphQL

您可以使用工具以相同的方式动态过滤结果:在方法上添加注释,从工具中访问它并动态处理结果:

public class BookService {

      @Filter("title ~ 'Monkey'") //example custom annotation
      public List<Book> findBooks(...) { /*get books from the DB */ }
}

new SimpleInstrumentation() {

    // You can also use beginFieldFetch and then onCompleted instead of instrumentDataFetcher
    @Override
    public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters) {
        GraphQLFieldDefinition field = parameters.getEnvironment().getFieldDefinition();
        Optional<String> filterExpression = Directives.getMappedOperation(field)
                .map(operation ->
                        operation.getApplicableResolver(parameters.getEnvironment().getArguments().keySet())
                                .getExecutable().getDelegate()
                                .getAnnotation(Filter.class).value()); //get the filtering expression from the annotation
        return filterExpression.isPresent() ? env -> filterResultBasedOn Expression(dataFetcher.get(parameters.getEnvironment()), filterExpression) : dataFetcher;
    }
}

对于类型的指令,再次使用Java批注。您可以通过以下方式访问基础类型:

Directives.getMappedType(graphQLType).getAnnotation(...);

这又可能仅在仪器中才有意义。之所以这样说是因为通常情况下,伪指令会提供额外的信息以将SDL映射到GraphQL类型。在SPQR中,您将Java类型映射到GraphQL类型,因此在大多数情况下,在这种情况下,指令是没有意义的。

当然,如果您仍然需要某个类型上的实际GraphQL指令,则始终可以提供一个自定义的TypeMapper来放置它们。

对于字段中的指令,当前在0.9.8中是不可能的。

0.9.9将在所有元素上提供完全的自定义指令支持,以防您仍然需要它们。

更新2: GraphQL SPQR 0.9.9已退出。

现在支持自定义指令。有关详细信息,请参见问题#200

任何用@GraphQLDirective进行元注释的自定义注释都将作为指令映射到被注释的元素上。

例如想象一个用来表示访问限制的自定义注释@Auth(requiredRole = "Admin")

@GraphQLDirective //Should be mapped as a GraphQLDirective
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) //Applicable to methods
public @interface Auth {
        String requiredRole();
}

如果使用@Auth注释了一个解析器方法:

@GraphQLMutation
@Auth(requiredRole = {"Admin"})
public Book addBook(Book newBook) { ... }

产生的GraphQL字段填充如下:

type Mutation {
  addBook(newBook: BookInput): Book @auth(requiredRole : "Admin")
}

也就是说,由于存在@Auth元注释,@GraphQLDirective注释已映射到指令。

可以通过以下方式添加客户指令:GraphQLSchemaGenerator#withAdditionalDirectives(java.lang.reflect.Type...)

SPQR 0.9.9还带有ResolverInterceptor,它们可以拦截解析器方法的调用并检查注释/指令。它们比Instrumentation更方便使用,但不那么通用(范围要有限得多)。有关详细信息,请参见问题#180,有关用法示例,请参见问题related tests

例如可以从上方使用@Auth注释(不是@Auth 不必是指令即可起作用)

public class AuthInterceptor implements ResolverInterceptor {

    @Override
    public Object aroundInvoke(InvocationContext context, Continuation continuation) throws Exception {
        Auth auth = context.getResolver().getExecutable().getDelegate().getAnnotation(Auth.class);
        User currentUser = context.getResolutionEnvironment().dataFetchingEnvironment.getContext();
        if (auth != null && !currentUser.getRoles().containsAll(Arrays.asList(auth.rolesRequired()))) {
            throw new IllegalAccessException("Access denied"); // or return null
            }
        return continuation.proceed(context);
    }
}

如果@Auth是指令,您还可以通过常规API获取它,例如

List<GraphQLDirective> directives = dataFetchingEnvironment.getFieldDefinition().get.getDirectives();
DirectivesUtil.directivesByName(directives);