CARVIEW |
Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New spec validation: Subscriptions root field must not contain @skip nor @include on root selection set #3871
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
Changes from all commits
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 |
---|---|---|
|
@@ -6,30 +6,38 @@ | |
import graphql.execution.FieldCollectorParameters; | ||
import graphql.execution.MergedField; | ||
import graphql.execution.MergedSelectionSet; | ||
import graphql.execution.RawVariables; | ||
import graphql.execution.ValuesResolver; | ||
import graphql.language.Directive; | ||
import graphql.language.NodeUtil; | ||
import graphql.language.OperationDefinition; | ||
import graphql.language.Selection; | ||
import graphql.language.VariableDefinition; | ||
import graphql.schema.GraphQLObjectType; | ||
import graphql.validation.AbstractRule; | ||
import graphql.validation.ValidationContext; | ||
import graphql.validation.ValidationErrorCollector; | ||
|
||
import java.util.List; | ||
|
||
import static graphql.Directives.INCLUDE_DIRECTIVE_DEFINITION; | ||
import static graphql.Directives.SKIP_DIRECTIVE_DEFINITION; | ||
import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; | ||
import static graphql.validation.ValidationErrorType.SubscriptionIntrospectionRootField; | ||
import static graphql.validation.ValidationErrorType.SubscriptionMultipleRootFields; | ||
import static graphql.validation.ValidationErrorType.ForbidSkipAndIncludeOnSubscriptionRoot; | ||
|
||
|
||
/** | ||
* A subscription operation must only have one root field | ||
* A subscription operation's single root field must not be an introspection field | ||
* https://spec.graphql.org/draft/#sec-Single-root-field | ||
* | ||
* A subscription operation's root field must not have neither @skip nor @include directives | ||
*/ | ||
@Internal | ||
public class SubscriptionUniqueRootField extends AbstractRule { | ||
public class SubscriptionRootField extends AbstractRule { | ||
private final FieldCollector fieldCollector = new FieldCollector(); | ||
public SubscriptionUniqueRootField(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { | ||
public SubscriptionRootField(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) { | ||
super(validationContext, validationErrorCollector); | ||
} | ||
|
||
|
@@ -39,16 +47,24 @@ public void checkOperationDefinition(OperationDefinition operationDef) { | |
|
||
GraphQLObjectType subscriptionType = getValidationContext().getSchema().getSubscriptionType(); | ||
|
||
// This coercion takes into account default values for variables | ||
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. This is fixing an edge case - this check previously didn't take into account default variable values because there was previously no coercion. That caused my tests to fail (see later) because I had set a default boolean value for include and skip. |
||
List<VariableDefinition> variableDefinitions = operationDef.getVariableDefinitions(); | ||
CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues( | ||
getValidationContext().getSchema(), | ||
variableDefinitions, | ||
RawVariables.emptyVariables(), | ||
getValidationContext().getGraphQLContext(), | ||
getValidationContext().getI18n().getLocale()); | ||
|
||
FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() | ||
.schema(getValidationContext().getSchema()) | ||
.fragments(NodeUtil.getFragmentsByName(getValidationContext().getDocument())) | ||
.variables(CoercedVariables.emptyVariables().toMap()) | ||
.variables(coercedVariableValues.toMap()) | ||
.objectType(subscriptionType) | ||
.graphQLContext(getValidationContext().getGraphQLContext()) | ||
.build(); | ||
|
||
MergedSelectionSet fields = fieldCollector.collectFields(collectorParameters, operationDef.getSelectionSet()); | ||
List<Selection> subscriptionSelections = operationDef.getSelectionSet().getSelections(); | ||
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. Unused variable |
||
|
||
if (fields.size() > 1) { | ||
String message = i18n(SubscriptionMultipleRootFields, "SubscriptionUniqueRootField.multipleRootFields", operationDef.getName()); | ||
|
@@ -57,16 +73,30 @@ public void checkOperationDefinition(OperationDefinition operationDef) { | |
|
||
MergedField mergedField = fields.getSubFieldsList().get(0); | ||
|
||
|
||
if (isIntrospectionField(mergedField)) { | ||
String message = i18n(SubscriptionIntrospectionRootField, "SubscriptionIntrospectionRootField.introspectionRootField", operationDef.getName(), mergedField.getName()); | ||
addError(SubscriptionIntrospectionRootField, mergedField.getSingleField().getSourceLocation(), message); | ||
} | ||
|
||
if (hasSkipOrIncludeDirectives(mergedField)) { | ||
String message = i18n(ForbidSkipAndIncludeOnSubscriptionRoot, "SubscriptionRootField.forbidSkipAndIncludeOnSubscriptionRoot", operationDef.getName(), mergedField.getName()); | ||
addError(ForbidSkipAndIncludeOnSubscriptionRoot, mergedField.getSingleField().getSourceLocation(), message); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private boolean isIntrospectionField(MergedField field) { | ||
return field.getName().startsWith("__"); | ||
} | ||
|
||
private boolean hasSkipOrIncludeDirectives(MergedField field) { | ||
List<Directive> directives = field.getSingleField().getDirectives(); | ||
for (Directive directive : directives) { | ||
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. Given recent performance improvements I am using plain for loops |
||
if (directive.getName().equals(SKIP_DIRECTIVE_DEFINITION.getName()) || directive.getName().equals(INCLUDE_DIRECTIVE_DEFINITION.getName())) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,9 +68,8 @@ ScalarLeaves.subselectionOnLeaf=Validation error ({0}) : Subselection not allowe | |
ScalarLeaves.subselectionRequired=Validation error ({0}) : Subselection required for type ''{1}'' of field ''{2}'' | ||
# | ||
SubscriptionUniqueRootField.multipleRootFields=Validation error ({0}) : Subscription operation ''{1}'' must have exactly one root field | ||
SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validation error ({0}) : Subscription operation ''{1}'' must have exactly one root field with fragments | ||
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. Unused messages |
||
SubscriptionIntrospectionRootField.introspectionRootField=Validation error ({0}) : Subscription operation ''{1}'' root field ''{2}'' cannot be an introspection field | ||
SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validation error ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' cannot be an introspection field | ||
SubscriptionRootField.forbidSkipAndIncludeOnSubscriptionRoot=Validation error ({0}) : Subscription operation ''{1}'' root field ''{2}'' must not use @skip nor @include directives in top level selection | ||
# | ||
UniqueArgumentNames.uniqueArgument=Validation error ({0}) : There can be only one argument named ''{1}'' | ||
# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,9 +60,9 @@ ScalarLeaves.subselectionOnLeaf=Validierungsfehler ({0}) : Unterauswahl für Bla | |
ScalarLeaves.subselectionRequired=Validierungsfehler ({0}) : Unterauswahl erforderlich für Typ ''{1}'' des Feldes ''{2}'' | ||
# | ||
SubscriptionUniqueRootField.multipleRootFields=Validierungsfehler ({0}) : Subscription operation ''{1}'' muss genau ein root field haben | ||
SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validierungsfehler ({0}) : Subscription operation ''{1}'' muss genau ein root field mit Fragmenten haben | ||
SubscriptionIntrospectionRootField.introspectionRootField=Validierungsfehler ({0}) : Subscription operation ''{1}'' root field ''{2}'' kann kein introspection field sein | ||
SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validierungsfehler ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' kann kein introspection field sein | ||
SubscriptionRootField.forbidSkipAndIncludeOnSubscriptionRoot=Validierungsfehler ({0}) : Subscription operation ''{1}'' root field ''{2}'' darf weder @skip noch @include directive in top level selection | ||
# | ||
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. Check my German please |
||
# | ||
UniqueArgumentNames.uniqueArgument=Validierungsfehler ({0}) : Es kann nur ein Argument namens ''{1}'' geben | ||
# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,9 +58,8 @@ ScalarLeaves.subselectionOnLeaf=Validatiefout ({0}) : Sub-selectie niet toegesta | |
ScalarLeaves.subselectionRequired=Validatiefout ({0}) : Sub-selectie verplicht voor type ''{1}'' van veld ''{2}'' | ||
# | ||
SubscriptionUniqueRootField.multipleRootFields=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field hebben | ||
SubscriptionUniqueRootField.multipleRootFieldsWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' moet exact één root field met fragmenten hebben | ||
SubscriptionIntrospectionRootField.introspectionRootField=Validatiefout ({0}) : Subscription operation ''{1}'' root field ''{2}'' kan geen introspectieveld zijn | ||
SubscriptionIntrospectionRootField.introspectionRootFieldWithFragment=Validatiefout ({0}) : Subscription operation ''{1}'' fragment root field ''{2}'' kan geen introspectieveld zijn | ||
SubscriptionRootField.forbidSkipAndIncludeOnSubscriptionRoot=Validation error ({0}) : Subscription operation ''{1}'' root field ''{2}'' must not use @skip nor @include directives in top level selection | ||
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. Add English placeholder |
||
# | ||
UniqueArgumentNames.uniqueArgument=Validatiefout ({0}) : Er mag maar één argument met naam ''{1}'' bestaan | ||
# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package graphql.validation.rules | ||
|
||
import graphql.parser.Parser | ||
import graphql.validation.SpecValidationSchema | ||
import graphql.validation.ValidationError | ||
import graphql.validation.Validator | ||
import spock.lang.Specification | ||
|
||
class SubscriptionRootFieldNoSkipNoIncludeTest extends Specification { | ||
|
||
def "valid subscription with @skip and @include directives on subfields"() { | ||
given: | ||
def query = """ | ||
subscription MySubscription(\$bool: Boolean = true) { | ||
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. This is what I mean by default values. When an empty coerced variables map is passed, this test fails |
||
dog { | ||
name @skip(if: \$bool) | ||
nickname @include(if: \$bool) | ||
} | ||
} | ||
""" | ||
|
||
when: | ||
def validationErrors = validate(query) | ||
|
||
then: | ||
validationErrors.isEmpty() | ||
} | ||
|
||
def "invalid subscription with @skip directive on root field"() { | ||
given: | ||
def query = """ | ||
subscription MySubscription(\$bool: Boolean = false) { | ||
dog @skip(if: \$bool) { | ||
name | ||
} | ||
dog @include(if: true) { | ||
nickname | ||
} | ||
} | ||
""" | ||
|
||
when: | ||
def validationErrors = validate(query) | ||
|
||
then: | ||
validationErrors.size() == 1 | ||
validationErrors.first().getMessage().contains("Subscription operation 'MySubscription' root field 'dog' must not use @skip nor @include directives in top level selection") | ||
} | ||
|
||
def "invalid subscription with @include directive on root field"() { | ||
given: | ||
def query = """ | ||
subscription MySubscription(\$bool: Boolean = true) { | ||
dog @include(if: \$bool) { | ||
name | ||
} | ||
} | ||
""" | ||
|
||
when: | ||
def validationErrors = validate(query) | ||
|
||
then: | ||
validationErrors.size() == 1 | ||
validationErrors.first().getMessage().contains("Subscription operation 'MySubscription' root field 'dog' must not use @skip nor @include directives in top level selection") | ||
} | ||
|
||
def "invalid subscription with directive on root field in fragment spread"() { | ||
given: | ||
def query = """ | ||
subscription MySubscription(\$bool: Boolean = false) { | ||
...dogFragment | ||
} | ||
|
||
fragment dogFragment on SubscriptionRoot { | ||
dog @skip(if: \$bool) { | ||
name | ||
} | ||
} | ||
""" | ||
|
||
when: | ||
def validationErrors = validate(query) | ||
|
||
then: | ||
validationErrors.size() == 1 | ||
validationErrors.first().getMessage().contains("Subscription operation 'MySubscription' root field 'dog' must not use @skip nor @include directives in top level selection") | ||
} | ||
|
||
def "invalid subscription with directive on root field in inline fragment"() { | ||
given: | ||
def query = """ | ||
subscription MySubscription(\$bool: Boolean = true) { | ||
... on SubscriptionRoot { | ||
dog @include(if: \$bool) { | ||
name | ||
} | ||
} | ||
} | ||
""" | ||
|
||
when: | ||
def validationErrors = validate(query) | ||
|
||
then: | ||
validationErrors.size() == 1 | ||
validationErrors.first().getMessage().contains("Subscription operation 'MySubscription' root field 'dog' must not use @skip nor @include directives in top level selection") | ||
} | ||
|
||
def "@skip and @include directives are valid on query root fields"() { | ||
given: | ||
def query = """ | ||
query MyQuery { | ||
pet @skip(if: false) { | ||
name | ||
} | ||
pet @include(if: true) { | ||
name | ||
} | ||
} | ||
""" | ||
|
||
when: | ||
def validationErrors = validate(query) | ||
|
||
then: | ||
validationErrors.size() == 0 | ||
} | ||
|
||
def "@skip and @include directives are valid on mutation root fields"() { | ||
given: | ||
def query = """ | ||
mutation MyMutation { | ||
createDog(input: {id: "a"}) @skip(if: false) { | ||
name | ||
} | ||
createDog(input: {id: "a"}) @include(if: true) { | ||
name | ||
} | ||
} | ||
""" | ||
|
||
when: | ||
def validationErrors = validate(query) | ||
|
||
then: | ||
validationErrors.size() == 0 | ||
} | ||
|
||
static List<ValidationError> validate(String query) { | ||
def document = new Parser().parseDocument(query) | ||
return new Validator().validateDocument(SpecValidationSchema.specValidationSchema, document, Locale.ENGLISH) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this rule name to be more generic, reflecting that it does 2 checks on the root field