Skip to content

Commit 7e6b579

Browse files
committed
adding defer validation
1 parent ce452d8 commit 7e6b579

9 files changed

Lines changed: 1046 additions & 0 deletions

src/main/java/graphql/validation/Validator.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import graphql.language.Document;
77
import graphql.schema.GraphQLSchema;
88
import graphql.validation.rules.ArgumentsOfCorrectType;
9+
import graphql.validation.rules.DeferDirectiveLabel;
10+
import graphql.validation.rules.DeferDirectiveOnRootLevel;
11+
import graphql.validation.rules.DeferDirectiveOnValidOperation;
912
import graphql.validation.rules.UniqueObjectFieldName;
1013
import graphql.validation.rules.ExecutableDefinitions;
1114
import graphql.validation.rules.FieldsOnCorrectType;
@@ -157,6 +160,15 @@ public List<AbstractRule> createRules(ValidationContext validationContext, Valid
157160
UniqueObjectFieldName uniqueObjectFieldName = new UniqueObjectFieldName(validationContext, validationErrorCollector);
158161
rules.add(uniqueObjectFieldName);
159162

163+
DeferDirectiveLabel deferDirectiveLabel = new DeferDirectiveLabel(validationContext, validationErrorCollector);
164+
rules.add(deferDirectiveLabel);
165+
166+
DeferDirectiveOnRootLevel deferDirectiveOnRootLevel = new DeferDirectiveOnRootLevel(validationContext, validationErrorCollector);
167+
rules.add(deferDirectiveOnRootLevel);
168+
169+
DeferDirectiveOnValidOperation deferDirectiveOnValidOperation = new DeferDirectiveOnValidOperation(validationContext, validationErrorCollector);
170+
rules.add(deferDirectiveOnValidOperation);
171+
160172
return rules;
161173
}
162174
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package graphql.validation.rules;
2+
3+
import graphql.Internal;
4+
import graphql.language.Argument;
5+
import graphql.language.Directive;
6+
import graphql.language.Node;
7+
import graphql.language.StringValue;
8+
import graphql.language.Value;
9+
import graphql.validation.AbstractRule;
10+
import graphql.validation.ValidationContext;
11+
import graphql.validation.ValidationErrorCollector;
12+
13+
import java.util.LinkedHashSet;
14+
import java.util.List;
15+
import java.util.Set;
16+
17+
import static graphql.validation.ValidationErrorType.DuplicateArgumentNames;
18+
19+
@Internal
20+
public class DeferDirectiveLabel extends AbstractRule {
21+
22+
private Set<String> labels = new LinkedHashSet<>();
23+
public DeferDirectiveLabel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
24+
super(validationContext, validationErrorCollector);
25+
}
26+
27+
@Override
28+
public void checkDirective(Directive directive, List<Node> ancestors) {
29+
if (!directive.getName().equals("defer") || directive.getArguments().size() == 0) {
30+
return;
31+
}
32+
33+
Argument labelArgument = directive.getArgument("label");
34+
// argument type is validated in DeferDirectiveArgumentType
35+
if (labelArgument != null && labelArgument.getValue() instanceof StringValue) {
36+
if (labels.contains(((StringValue) labelArgument.getValue()).getValue())) {
37+
String message = i18n(DuplicateArgumentNames, "UniqueArgumentNames.directiveUniqueArgument", labelArgument.getName(), directive.getName());
38+
addError(DuplicateArgumentNames, directive.getSourceLocation(), message);
39+
} else {
40+
labels.add(((StringValue) labelArgument.getValue()).getValue() );
41+
}
42+
}
43+
}
44+
45+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package graphql.validation.rules;
2+
3+
import graphql.language.Directive;
4+
import graphql.language.Document;
5+
import graphql.language.FragmentDefinition;
6+
import graphql.language.FragmentSpread;
7+
import graphql.language.InlineFragment;
8+
import graphql.language.Node;
9+
import graphql.language.OperationDefinition;
10+
import graphql.language.SelectionSet;
11+
import graphql.validation.AbstractRule;
12+
import graphql.validation.DocumentVisitor;
13+
import graphql.validation.LanguageTraversal;
14+
import graphql.validation.ValidationContext;
15+
import graphql.validation.ValidationErrorCollector;
16+
17+
import java.util.ArrayList;
18+
import java.util.Collections;
19+
import java.util.LinkedHashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Optional;
23+
24+
import static graphql.validation.ValidationErrorType.MisplacedDirective;
25+
26+
public class DeferDirectiveOnRootLevel extends AbstractRule {
27+
28+
public DeferDirectiveOnRootLevel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
29+
super(validationContext, validationErrorCollector);
30+
this.setVisitFragmentSpreads(true);
31+
}
32+
33+
@Override
34+
public void checkDirective(Directive directive, List<Node> ancestors) {
35+
if (directive.getName().equals("defer")){
36+
Optional<Node> fragmentAncestor = getFragmentAncestor(ancestors);
37+
if (fragmentAncestor.isPresent() && fragmentAncestor.get() instanceof OperationDefinition){
38+
OperationDefinition operationDefinition = (OperationDefinition) fragmentAncestor.get();
39+
40+
if (OperationDefinition.Operation.MUTATION.equals(operationDefinition.getOperation())) {
41+
String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), OperationDefinition.Operation.MUTATION.name().toLowerCase());
42+
addError(MisplacedDirective, directive.getSourceLocation(), message);
43+
} else if (OperationDefinition.Operation.SUBSCRIPTION.equals(operationDefinition.getOperation())) {
44+
String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperationRootLevel", directive.getName(), OperationDefinition.Operation.SUBSCRIPTION.name().toLowerCase());
45+
addError(MisplacedDirective, directive.getSourceLocation(), message);
46+
}
47+
}
48+
49+
}
50+
}
51+
52+
/**
53+
* Get the first ancestor that is not InlineFragment, SelectionSet or FragmentDefinition
54+
* @param ancestors list of ancestors
55+
* @return Optional of Node parent that is not InlineFragment, SelectionSet or FragmentDefinition.
56+
*/
57+
protected Optional<Node> getFragmentAncestor(List<Node> ancestors){
58+
List<Node> ancestorsCopy = new ArrayList(ancestors);
59+
Collections.reverse(ancestorsCopy);
60+
return ancestorsCopy.stream().filter(node -> !(
61+
node instanceof InlineFragment ||
62+
node instanceof SelectionSet ||
63+
node instanceof FragmentDefinition
64+
)
65+
).findFirst();
66+
67+
}
68+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package graphql.validation.rules;
2+
3+
import graphql.language.BooleanValue;
4+
import graphql.language.Directive;
5+
import graphql.language.Document;
6+
import graphql.language.Node;
7+
import graphql.language.OperationDefinition;
8+
import graphql.validation.AbstractRule;
9+
import graphql.validation.ValidationContext;
10+
import graphql.validation.ValidationErrorCollector;
11+
12+
import java.util.List;
13+
import java.util.Optional;
14+
15+
import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION;
16+
import static graphql.validation.ValidationErrorType.MisplacedDirective;
17+
18+
/**
19+
* Defer Directive is Used On Valid Operations
20+
*
21+
* A GraphQL document is only valid if defer directives are not used on subscription types.
22+
*/
23+
public class DeferDirectiveOnValidOperation extends AbstractRule {
24+
25+
public DeferDirectiveOnValidOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
26+
super(validationContext, validationErrorCollector);
27+
this.setVisitFragmentSpreads(true);
28+
}
29+
30+
@Override
31+
public void documentFinished(Document document) {
32+
super.documentFinished(document);
33+
}
34+
35+
@Override
36+
public void checkDirective(Directive directive, List<Node> ancestors) {
37+
if (!directive.getName().equals("defer") ||
38+
(directive.getArgumentsByName().get("if") != null && !((BooleanValue) directive.getArgumentsByName().get("if").getValue()).isValue() )) {
39+
return;
40+
}
41+
// check if the directive is on allowed operation
42+
Optional<OperationDefinition> operationDefinition = getOperation(ancestors);
43+
if (operationDefinition.isPresent() && operationDefinition.get().getOperation() == SUBSCRIPTION) {
44+
String message = i18n(MisplacedDirective, "DirectiveMisplaced.notAllowedOperation", directive.getName(), SUBSCRIPTION.name().toLowerCase());
45+
addError(MisplacedDirective, directive.getSourceLocation(), message);
46+
}
47+
}
48+
49+
50+
/**
51+
* Extract from ancestors the OperationDefinition using the document ancestor.
52+
* @param ancestors list of ancestors
53+
* @return OperationDefinition
54+
*/
55+
protected Optional<OperationDefinition> getOperation(List<Node> ancestors) {
56+
return ancestors.stream()
57+
.filter(doc -> doc instanceof OperationDefinition)
58+
.map((def -> (OperationDefinition) def))
59+
.findFirst();
60+
}
61+
62+
}
63+

src/main/resources/i18n/Validation.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ SubscriptionIntrospectionRootField.introspectionRootField=Validation error ({0})
6363
SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validation error ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' cannot be an introspection field
6464
#
6565
UniqueArgumentNames.uniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}''
66+
67+
UniqueArgumentNames.directiveUniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}'' for directive ''{2}''
6668
#
6769
UniqueDirectiveNamesPerLocation.uniqueDirectives=Validation error ({0}) : Non repeatable directives must be uniquely named within a location. The directive ''{1}'' used on a ''{2}'' is not unique
6870
#
@@ -98,3 +100,6 @@ ArgumentValidationUtil.handleNotObjectError=Validation error ({0}) : argument ''
98100
ArgumentValidationUtil.handleMissingFieldsError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' is missing required fields ''{3}''
99101
# suppress inspection "UnusedProperty"
100102
ArgumentValidationUtil.handleExtraFieldError=Validation error ({0}) : argument ''{1}'' with value ''{2}'' contains a field not in ''{3}'': ''{4}''
103+
104+
DirectiveMisplaced.notAllowedOperationRootLevel=Validation error ({0}) : Directive ''{1}'' is not allowed on root of operation ''{2}''
105+
DirectiveMisplaced.notAllowedOperation=Validation error ({0}) : Directive ''{1}'' is not on operation ''{2}''

src/test/groovy/graphql/validation/SpecValidationSchema.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
import static graphql.introspection.Introspection.DirectiveLocation.QUERY;
3131
import static graphql.schema.GraphQLArgument.newArgument;
3232
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
33+
import static graphql.schema.GraphQLInputObjectField.newInputObjectField;
34+
import static graphql.schema.GraphQLInputObjectType.newInputObject;
3335
import static graphql.schema.GraphQLNonNull.nonNull;
36+
import static graphql.schema.GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY;
3437
import static java.util.Collections.singletonList;
3538

3639
/**
@@ -215,6 +218,25 @@ public class SpecValidationSchema {
215218
.field(newFieldDefinition().name("cat").type(cat))
216219
.build();
217220

221+
public static GraphQLInputObjectType inputDogType = newInputObject()
222+
.name("DogInput")
223+
.description("Input for A Dog creation.")
224+
.field(newInputObjectField()
225+
.name("id")
226+
.description("The id of the dog.")
227+
.type(nonNull(GraphQLString)))
228+
.build();
229+
230+
public static final GraphQLObjectType petMutationType = GraphQLObjectType.newObject()
231+
.name("PetMutationType")
232+
.field(newFieldDefinition()
233+
.name("createDog")
234+
.type(dog)
235+
.argument(newArgument()
236+
.name("input")
237+
.type(inputDogType)))
238+
.build();
239+
218240
public static final Set<GraphQLType> specValidationDictionary = new HashSet<GraphQLType>() {{
219241
add(dogCommand);
220242
add(catCommand);
@@ -275,11 +297,13 @@ public class SpecValidationSchema {
275297
.query(queryRoot)
276298
.codeRegistry(codeRegistry)
277299
.subscription(subscriptionRoot)
300+
.mutation(petMutationType)
278301
.additionalDirective(upperDirective)
279302
.additionalDirective(lowerDirective)
280303
.additionalDirective(dogDirective)
281304
.additionalDirective(nonNullDirective)
282305
.additionalDirective(objectArgumentDirective)
306+
.additionalDirective(Directives.DeferDirective)
283307
.additionalTypes(specValidationDictionary)
284308
.build();
285309

0 commit comments

Comments
 (0)