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
可适用于各种类型的校验,实现对应方法可对Field
、Argument
、Interface
等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()));
}
}
}
参考资料: