GraphQL(五)指令[Directive]详解

GraphQL Directive(指令)是GraphQL中的一种特殊类型,它允许开发者在GraphQL schema中添加元数据,以控制查询和解析操作的行为

Directive由@符号和名称组成,例如@deprecated。Directive可以具有参数,这些参数可以用于更精细地控制Directive的行为

在GraphQL中,有一些内置的Directive,例如@deprecated@skip@include。其中,@deprecated用于标记不建议使用的字段;@skip@include用于控制查询中是否包含某些字段

指令位置

目前对指令的支持仅限于以下位置:

  • OBJECT
  • FIELD_DEFINITION
  • ARGUMENT_DEFINITION
  • INTERFACE
  • UNION
  • ENUM
  • ENUM_VALUE
  • INPUT_OBJECT
  • INPUT_FIELD_DEFINITION

目前尚不支持以下位置的含义指令:

  • SCALAR

拓展指令

graphql-java-extended-validation 库为graphql-java提供字段和字段参数的扩展验证

该库名称和语义的灵感来自javax.validation注释

# 例子:限制输入的字符数
# this declares the directive as being possible on arguments and input fields
#
directive @Size(min : Int = 0, max : Int = 2147483647, message : String = "graphql.validation.Size.message")
                    on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION

input Application {
    name : String @Size(min : 3, max : 100)
}

type Query {
    hired (applications : [Application!] @Size(max : 10)) : [Boolean]
}

架构指令接线

配置ValidationSchemaWiring以启动指令验证

@Configuration
public class GraphQLSchemaConfiguration {
    @DgsComponent
    public class SecuredDirectiveRegistration {

        @DgsRuntimeWiring
        public RuntimeWiring.Builder addSecuredDirective(RuntimeWiring.Builder builder) {
            return builder.directiveWiring(new ValidationSchemaWiring(ValidationRules.newValidationRules().addRule(new DateRangeRule())
                            .onValidationErrorStrategy(OnValidationErrorStrategy.RETURN_NULL)
                            .build()));
        }
    }
}

在底层,ValidationSchemaWiring会在遇到每个字段时询问每个可能的规则是否适用于该字段(在schema构建时)。如果它们适用,则它会重写DataFetcher,使其首先调用验证代码并在字段输入不被视为有效时生成错误

如果字段输入被视为无效,默认策略OnValidationErrorStrategy.RETURN_NULL会返回null

可拓展指令

  • @AssertFalse
  • @AssertTrue
  • @Size
  • @ContainerSize
  • @Max
  • @Range
  • @NotEmpty

更多拓展指令及具体用法可见@Directive 约束

Java EL @Expression

验证指令@Expression允许使用Java EL来帮助构建验证规则

Java EL 表达式必须评估为布尔值才能在@Expresion指令中使用

EL表达式 结果
${1> (4/2)} false
${4.0>= 3} true
${100.0 == 100} true
${(10*10) ne 100} false
${'a' > 'b'} false
${'hip' lt 'hit'} true
${4> 3} true
${1.2E4 + 1.4} 12001.4
${3 div 4} 0.75
${10 mod 4} 2
${((x, y) → x + y)(3, 5.5)} 8.5
[1,2,3,4].stream().sum() 10
[1,3,5,2].stream().sorted().toList() [1, 2, 3, 5]

可以使用以下验证变量

名称
validatedValue 正在验证的值
gqlField 正在验证的GraphQLFieldDefinition
gqlFieldContainer GraphQLFieldsContainer的父类型包含的字段
gqlArgument 正在验证中的GraphQLArgument。对于字段级验证,这可以为null
arguments 当前字段的所有参数值的映射
args 当前字段的所有参数值的映射的简写名称

详细介绍及使用见Java Expression Language

自定义指令

以下为两种方式的参数校验指令例子。对于请求参数的校验,推荐使用ValidationRule方式,而SchemaDirectiveWiring可适用于各种DSL元素的校验

SchemaDirectiveWiring

SchemaDirectiveWiring可适用于各种类型的校验,实现对应方法可对FieldArgumentInterface等DSL元素进行校验

Schema

"月份在cnt月以内,校验String类型yyyyMM/yyyy-MM 及 Date类型yyyy-MM-dd"
directive @withinMonth(cnt: Int!) on ARGUMENT_DEFINITION


type Employee {
    "雇员名称 只能查最近12个月"
    employees(month: Date @withinMonth(12) : [String]
}

MonthCheckDirective

@Component
public class MonthCheckDirective implements SchemaDirectiveWiring {
    private static final Logger LOGGER = LoggerFactory.getLogger(MonthCheckDirective.class);

    public static final String MONTH_DIRECTIVE = "withinMonth";
    public static final String CNT_ATTR = "cnt";

    /**
     * argument校验,不满足条件抛出异常
     * @param environment
     * @return
     */
    @Override
    public GraphQLArgument onArgument(SchemaDirectiveWiringEnvironment<GraphQLArgument> environment) {
        if (environment.getAppliedDirectives().get(MONTH_DIRECTIVE) == null) {
            return environment.getElement();
        }

        // 原始DataFetcher,无需修改参数值时,最后需返回原始DataFetcher的值
        DataFetcher originalDataFetcher = environment.getCodeRegistry().getDataFetcher(environment.getFieldsContainer(), environment.getFieldDefinition());
        Object value = environment.getAppliedDirectives().get(MONTH_DIRECTIVE).getArgument(CNT_ATTR).getValue();
        int months = (Integer) value;
        GraphQLArgument argument = environment.getElement();
        environment.getCodeRegistry().dataFetcher(
                FieldCoordinates.coordinates(environment.getFieldsContainer().getName(), environment.getFieldDefinition().getName()),
                (DataFetcher<Object>) env -> {
                    Object argumentValue = env.getArgument(argument.getName());
                    if (argumentValue == null) {
                        return originalDataFetcher.get(env);
                    }

                    if (argumentValue instanceof String month) {
                        month = month.replaceAll("-", "");
                        String dateStr = month.substring(0, 4) + "-" + month.substring(4) + "-01";
                        argumentValue = LocalDate.parse(dateStr);
                    }
                    if (argumentValue instanceof LocalDate date) {
                        if (date.isBefore(LocalDate.now().minusMonths(months).withDayOfMonth(1))) {
                            LOGGER.info("month check fail");
                            throw new MonthCheckRuntimeException(months);
                        }
                    }
                    return originalDataFetcher.get(env);
                }
        );
        return argument;
    }
}

GraphQLSchemaConfiguration

@Configuration
public class GraphQLSchemaConfiguration {
    @DgsComponent
    public class SecuredDirectiveRegistration {
        
        private MonthCheckDirective monthCheckDirective;
        public SecuredDirectiveRegistration(MonthCheckDirective monthCheckDirective) {
            this.monthCheckDirective = monthCheckDirective;
        }

        @DgsRuntimeWiring
        public RuntimeWiring.Builder addSecuredDirective(RuntimeWiring.Builder builder) {
            return builder.directive(MonthCheckDirective.MONTH_DIRECTIVE,monthCheckDirective);
        }
    }
}

ValidationRule

ValidationRule方式的自定义验证规则

Schema

directive @DateRange(min : Int = -360 , max : Int = 0, unit: DateRangeUnit = day , message : String = "{path} size must be between {min} and {max}") on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION

type Employee {
    "雇员名称 只能查12个月前~上个月"
    employees(month: Date @DateRange(min: -12, max: -1, unit: month) : [String]
}

DateRangeRule

public class DateRangeRule  extends AbstractDirectiveConstraint {

    public DateRangeRule() {
        super("DateRange");
    }

    @Override
    public Documentation getDocumentation() {
        return Documentation.newDocumentation()
                .messageTemplate(getMessageTemplate())
                .description("用来限制 date 类型范围.")
                .example("driver( milesTravelled : Int @DateRange( min : -1, unit: month)) : DriverDetails")
                .applicableTypes(GRAPHQL_NUMBER_AND_STRING_TYPES)
                .directiveSDL("enum DateRangeUnit{ day \n month} \n directive @DateRange(min : Int = 0 , unit: DateRangeUnit=day , max : Int = %d , message : String = \"%s\") " +
                                "on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION",
                        Integer.MAX_VALUE, getMessageTemplate())
                .build();
    }

	/**
	 * 只适用于Date类型参数
	 */
    @Override
    public boolean appliesToType(GraphQLInputType inputType) {
        GraphQLInputType type = Util.unwrapNonNull(inputType);
        if (type instanceof GraphQLNamedInputType) {
            final GraphQLNamedInputType unwrappedType = (GraphQLNamedInputType) type;
            return unwrappedType.getName().equals(ExtendedScalars.Date.getName());
        }
        return false;
    }

    @Override
    protected List<GraphQLError> runConstraint(ValidationEnvironment validationEnvironment) {
        Object validatedValue = validationEnvironment.getValidatedValue();
        GraphQLAppliedDirective directive = validationEnvironment.getContextObject(GraphQLAppliedDirective.class);
        int min = getIntArg(directive, "min");
        String unit = getStrArg(directive,"unit");
        int max = getIntArg(directive, "max");
        boolean isOK;
        LocalDate validatedDate = (LocalDate) validatedValue;
        LocalDate now = LocalDate.now();
        LocalDate maxDate;
        LocalDate minDate;
        if("day".equals(unit)){
            maxDate = now.plusDays(max);
            minDate = now.plusDays(min);
        }else{
            maxDate = now.plusMonths(max);
            minDate = now.plusMonths(min).withDayOfMonth(1);
        }
        isOK = ( validatedDate.isBefore(maxDate) || validatedDate.isEqual(maxDate) )&& (validatedDate.isAfter(minDate) || validatedDate.isEqual(minDate));
        if (!isOK) {
            return mkError(validationEnvironment, "min", minDate, "max", maxDate);

        }
        return Collections.emptyList();
    }

    @Override
    protected boolean appliesToListElements() {
        return false;
    }
}

GraphQLSchemaConfiguration

@Configuration
public class GraphQLSchemaConfiguration {
    @DgsComponent
    public class SecuredDirectiveRegistration {

        @DgsRuntimeWiring
        public RuntimeWiring.Builder addSecuredDirective(RuntimeWiring.Builder builder) {
            return builder.directiveWiring(new ValidationSchemaWiring(ValidationRules.newValidationRules().addRule(new DateRangeRule())
                            .onValidationErrorStrategy(OnValidationErrorStrategy.RETURN_NULL)
                            .build()));
        }
    }
}

参考资料:

  1. graphql-java-extended-validation
  2. SDL 指令
  3. Directives

你可能感兴趣的:(GraphQL,graphql,java,Directive,指令,参数校验)