-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Defer validation #3439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Defer validation #3439
Changes from all commits
7e6b579
2f2b794
63e344e
affc1b8
10ff520
8d382e6
55902ab
f388fc6
5b517cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package graphql.validation.rules; | ||
|
|
||
| import graphql.Directives; | ||
| import graphql.ExperimentalApi; | ||
| import graphql.language.Argument; | ||
| import graphql.language.Directive; | ||
| import graphql.language.Node; | ||
| import graphql.language.NullValue; | ||
| import graphql.language.StringValue; | ||
| import graphql.language.Value; | ||
| import graphql.validation.AbstractRule; | ||
| import graphql.validation.ValidationContext; | ||
| import graphql.validation.ValidationErrorCollector; | ||
|
|
||
| import java.util.LinkedHashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| import static graphql.validation.ValidationErrorType.DuplicateArgumentNames; | ||
| import static graphql.validation.ValidationErrorType.DuplicateIncrementalLabel; | ||
| import static graphql.validation.ValidationErrorType.VariableNotAllowed; | ||
| import static graphql.validation.ValidationErrorType.WrongType; | ||
|
|
||
| /** | ||
| * Defer and stream directive labels are unique | ||
| * | ||
| * A GraphQL document is only valid if defer and stream directives' label argument is static and unique. | ||
| * | ||
| * See proposed spec:<a href="https://github.com/graphql/graphql-spec/pull/742">spec/Section 5 -- Validation.md ### ### Defer And Stream Directive Labels Are Unique</a> | ||
| */ | ||
| @ExperimentalApi | ||
| public class DeferDirectiveLabel extends AbstractRule { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use a more descriptive name, like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it doesn't only validate if label is unique. It also validades if label is not StringValue (static string) |
||
| private Set<String> checkedLabels = new LinkedHashSet<>(); | ||
| public DeferDirectiveLabel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { | ||
| super(validationContext, validationErrorCollector); | ||
| } | ||
|
|
||
| @Override | ||
| public void checkDirective(Directive directive, List<Node> ancestors) { | ||
| // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true | ||
| if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT) || | ||
| !Directives.DeferDirective.getName().equals(directive.getName()) || | ||
| directive.getArguments().size() == 0) { | ||
| return; | ||
| } | ||
|
|
||
| Argument labelArgument = directive.getArgument("label"); | ||
| if (labelArgument == null || labelArgument.getValue() instanceof NullValue){ | ||
| return; | ||
| } | ||
| Value labelArgumentValue = labelArgument.getValue(); | ||
|
|
||
| if (!(labelArgumentValue instanceof StringValue)) { | ||
| String message = i18n(WrongType, "DeferDirective.labelMustBeStaticString"); | ||
| addError(WrongType, directive.getSourceLocation(), message); | ||
| } else { | ||
| if (checkedLabels.contains(((StringValue) labelArgumentValue).getValue())) { | ||
| String message = i18n(DuplicateIncrementalLabel, "IncrementalDirective.uniqueArgument", labelArgument.getName(), directive.getName()); | ||
| addError(DuplicateIncrementalLabel, directive.getSourceLocation(), message); | ||
| } else { | ||
| checkedLabels.add(((StringValue) labelArgumentValue).getValue()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package graphql.validation.rules; | ||
|
|
||
| import graphql.Directives; | ||
| import graphql.ExperimentalApi; | ||
| import graphql.language.Directive; | ||
| import graphql.language.Node; | ||
| import graphql.language.OperationDefinition; | ||
| import graphql.schema.GraphQLCompositeType; | ||
| import graphql.schema.GraphQLObjectType; | ||
| import graphql.validation.AbstractRule; | ||
| import graphql.validation.ValidationContext; | ||
| import graphql.validation.ValidationErrorCollector; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.LinkedHashSet; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
|
|
||
| import static graphql.validation.ValidationErrorType.MisplacedDirective; | ||
|
|
||
| /** | ||
| * Defer and stream directives are used on valid root field | ||
| * | ||
| * A GraphQL document is only valid if defer directives are not used on root mutation or subscription types. | ||
| * | ||
| * See proposed spec:<a href="https://github.com/graphql/graphql-spec/pull/742">spec/Section 5 -- Validation.md ### Defer And Stream Directives Are Used On Valid Root Field</a> | ||
| */ | ||
| @ExperimentalApi | ||
| public class DeferDirectiveOnRootLevel extends AbstractRule { | ||
| private Set<OperationDefinition.Operation> invalidOperations = new LinkedHashSet(Arrays.asList(OperationDefinition.Operation.MUTATION, OperationDefinition.Operation.SUBSCRIPTION)); | ||
| public DeferDirectiveOnRootLevel(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { | ||
| super(validationContext, validationErrorCollector); | ||
| this.setVisitFragmentSpreads(true); | ||
| } | ||
|
|
||
| @Override | ||
| public void checkDirective(Directive directive, List<Node> ancestors) { | ||
| // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true | ||
| if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { | ||
| return; | ||
| } | ||
|
|
||
| if (!Directives.DeferDirective.getName().equals(directive.getName())) { | ||
| return; | ||
| } | ||
| GraphQLObjectType mutationType = getValidationContext().getSchema().getMutationType(); | ||
| GraphQLObjectType subscriptionType = getValidationContext().getSchema().getSubscriptionType(); | ||
| GraphQLCompositeType parentType = getValidationContext().getParentType(); | ||
Juliano-Prado marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (mutationType != null && parentType != null && parentType.getName().equals(mutationType.getName())){ | ||
| String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelMutation", parentType.getName()); | ||
| addError(MisplacedDirective, directive.getSourceLocation(), message); | ||
| } else if (subscriptionType != null && parentType != null && parentType.getName().equals(subscriptionType.getName())) { | ||
| String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelSubscription", parentType.getName()); | ||
| addError(MisplacedDirective, directive.getSourceLocation(), message); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package graphql.validation.rules; | ||
|
|
||
| import graphql.Directives; | ||
| import graphql.ExperimentalApi; | ||
| import graphql.language.Argument; | ||
| import graphql.language.BooleanValue; | ||
| import graphql.language.Directive; | ||
| import graphql.language.Node; | ||
| import graphql.language.OperationDefinition; | ||
| import graphql.language.VariableReference; | ||
| import graphql.validation.AbstractRule; | ||
| import graphql.validation.ValidationContext; | ||
| import graphql.validation.ValidationErrorCollector; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; | ||
| import static graphql.validation.ValidationErrorType.MisplacedDirective; | ||
|
|
||
| /** | ||
Juliano-Prado marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * Defer Directive is Used On Valid Operations | ||
| * | ||
| * A GraphQL document is only valid if defer directives are not used on subscription types. | ||
| * | ||
| * See proposed spec:<a href="https://github.com/graphql/graphql-spec/pull/742">spec/Section 5 -- Validation.md ### Defer And Stream Directives Are Used On Valid Operations</a> | ||
| * | ||
| */ | ||
| @ExperimentalApi | ||
| public class DeferDirectiveOnValidOperation extends AbstractRule { | ||
| public DeferDirectiveOnValidOperation(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { | ||
| super(validationContext, validationErrorCollector); | ||
| this.setVisitFragmentSpreads(true); | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public void checkDirective(Directive directive, List<Node> ancestors) { | ||
| // ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT must be true | ||
| if (!isExperimentalApiKeyEnabled(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT)) { | ||
| return; | ||
| } | ||
|
|
||
| if (!Directives.DeferDirective.getName().equals(directive.getName())) { | ||
| return; | ||
| } | ||
Juliano-Prado marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // check if the directive is on allowed operation | ||
| Optional<OperationDefinition> operationDefinition = getOperationDefinition(ancestors); | ||
| if (operationDefinition.isPresent() && | ||
| SUBSCRIPTION.equals(operationDefinition.get().getOperation()) && | ||
| !ifArgumentMightBeFalse(directive) ){ | ||
| String message = i18n(MisplacedDirective, "IncrementalDirective.notAllowedSubscriptionOperation", directive.getName()); | ||
| addError(MisplacedDirective, directive.getSourceLocation(), message); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Extract from ancestors the OperationDefinition using the document ancestor. | ||
| * @param ancestors list of ancestors | ||
| * @return Optional of OperationDefinition | ||
| */ | ||
| private Optional<OperationDefinition> getOperationDefinition(List<Node> ancestors) { | ||
| return ancestors.stream() | ||
| .filter(doc -> doc instanceof OperationDefinition) | ||
| .map((def -> (OperationDefinition) def)) | ||
| .findFirst(); | ||
| } | ||
|
|
||
| private Boolean ifArgumentMightBeFalse(Directive directive) { | ||
| Argument ifArgument = directive.getArgumentsByName().get("if"); | ||
| if (ifArgument == null) { | ||
| return false; | ||
| } | ||
| if(ifArgument.getValue() instanceof BooleanValue){ | ||
| return !((BooleanValue) ifArgument.getValue()).isValue(); | ||
| } | ||
| if(ifArgument.getValue() instanceof VariableReference){ | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -647,34 +647,6 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { | |
| ] | ||
| } | ||
|
|
||
| def "multiple fields and a multiple defers with same label are not allowed"() { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. validation is already catching as invalid query |
||
| given: | ||
|
|
||
| String query = ''' | ||
| query q { | ||
| dog { | ||
| ... @defer(label:"name-defer") { | ||
| name | ||
| } | ||
|
|
||
| ... @defer(label:"name-defer") { | ||
| name | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ''' | ||
|
|
||
| Map<String, Object> variables = [:] | ||
|
|
||
| when: | ||
| executeQueryAndPrintTree(query, variables) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. query is set as invalid on validation, test was failing |
||
|
|
||
| then: | ||
| def exception = thrown(AssertException) | ||
| exception.message == "Internal error: should never happen: Duplicated @defer labels are not allowed: [name-defer]" | ||
| } | ||
|
|
||
| def "nested defers - no label"() { | ||
| given: | ||
|
|
||
|
|
@@ -861,35 +833,6 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { | |
| ] | ||
| } | ||
|
|
||
| def "'if' argument with different values on same field and same label"() { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. validation is already catching as invalid query |
||
| given: | ||
|
|
||
| String query = ''' | ||
| query q { | ||
| dog { | ||
| ... @defer(if: false, label: "name-defer") { | ||
| name | ||
| } | ||
|
|
||
| ... @defer(if: true, label: "name-defer") { | ||
| name | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ''' | ||
|
|
||
| Map<String, Object> variables = [:] | ||
|
|
||
| when: | ||
| List<String> printedTree = executeQueryAndPrintTree(query, variables) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. query is set as invalid on validation, test was failing |
||
|
|
||
| then: | ||
| printedTree == ['Query.dog', | ||
| 'Dog.name defer{[label=name-defer;types=[Dog]]}', | ||
| ] | ||
| } | ||
|
|
||
| private ExecutableNormalizedOperation createExecutableNormalizedOperations(String query, Map<String, Object> variables) { | ||
| assertValidQuery(graphQLSchema, query, variables) | ||
| Document document = TestUtil.parseQuery(query) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.