Skip to content

Commit 10eb985

Browse files
authored
Validation of input and output types only in the right context (graphql-java#2664)
* Validation of input and output types in the right context * extra test conditions * Fixed test
1 parent 0eaeb35 commit 10eb985

5 files changed

Lines changed: 189 additions & 2 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package graphql.schema.validation;
2+
3+
import graphql.Internal;
4+
import graphql.schema.GraphQLArgument;
5+
import graphql.schema.GraphQLFieldDefinition;
6+
import graphql.schema.GraphQLInputObjectField;
7+
import graphql.schema.GraphQLInputType;
8+
import graphql.schema.GraphQLModifiedType;
9+
import graphql.schema.GraphQLOutputType;
10+
import graphql.schema.GraphQLSchemaElement;
11+
import graphql.schema.GraphQLType;
12+
import graphql.schema.GraphQLTypeUtil;
13+
import graphql.schema.GraphQLTypeVisitorStub;
14+
import graphql.util.TraversalControl;
15+
import graphql.util.TraverserContext;
16+
17+
import java.util.function.BiFunction;
18+
import java.util.function.Predicate;
19+
20+
/**
21+
* Schema validation rule ensuring no input type forms an unbroken non-nullable recursion,
22+
* as such a type would be impossible to satisfy
23+
*/
24+
@Internal
25+
public class InputAndOutputTypesUsedAppropriately extends GraphQLTypeVisitorStub {
26+
27+
@Override
28+
public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDef, TraverserContext<GraphQLSchemaElement> context) {
29+
String typeName = getTypeName((GraphQLType) context.getParentNode());
30+
String fieldName = typeName + "." + fieldDef.getName();
31+
SchemaValidationErrorCollector validationErrorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
32+
for (GraphQLArgument argument : fieldDef.getArguments()) {
33+
String argName = fieldName + "." + argument.getName();
34+
GraphQLInputType argumentType = argument.getType();
35+
checkIsAllInputTypes(argumentType, validationErrorCollector, argName);
36+
}
37+
checkIsAllOutputTypes(fieldDef.getType(), validationErrorCollector, fieldName);
38+
return TraversalControl.CONTINUE;
39+
}
40+
41+
@Override
42+
public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField fieldDef, TraverserContext<GraphQLSchemaElement> context) {
43+
String typeName = getTypeName((GraphQLType) context.getParentNode());
44+
String fieldName = typeName + "." + fieldDef.getName();
45+
SchemaValidationErrorCollector validationErrorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class);
46+
checkIsAllInputTypes(fieldDef.getType(), validationErrorCollector, fieldName);
47+
return TraversalControl.CONTINUE;
48+
}
49+
50+
private void checkIsAllInputTypes(GraphQLInputType inputType,
51+
SchemaValidationErrorCollector validationErrorCollector,
52+
String argName) {
53+
checkTypeContext(inputType, validationErrorCollector, argName,
54+
typeToCheck -> typeToCheck instanceof GraphQLInputType,
55+
(typeToCheck, path) -> new SchemaValidationError(SchemaValidationErrorType.OutputTypeUsedInInputTypeContext,
56+
String.format("The output type '%s' has been used in an input type context : '%s'", typeToCheck, path)));
57+
}
58+
59+
private void checkIsAllOutputTypes(GraphQLOutputType outputType,
60+
SchemaValidationErrorCollector validationErrorCollector,
61+
String fieldName) {
62+
checkTypeContext(outputType, validationErrorCollector, fieldName,
63+
typeToCheck -> typeToCheck instanceof GraphQLOutputType,
64+
(typeToCheck, path) -> new SchemaValidationError(SchemaValidationErrorType.InputTypeUsedInOutputTypeContext,
65+
String.format("The input type '%s' has been used in a output type context : '%s'", typeToCheck, path)));
66+
}
67+
68+
private void checkTypeContext(GraphQLType type,
69+
SchemaValidationErrorCollector validationErrorCollector,
70+
String path,
71+
Predicate<GraphQLType> typePredicate,
72+
BiFunction<String, String, SchemaValidationError> errorMaker) {
73+
while (true) {
74+
String typeName = getTypeName(type);
75+
boolean isOk = typePredicate.test(type);
76+
if (!isOk) {
77+
validationErrorCollector.addError(errorMaker.apply(typeName, path));
78+
}
79+
if (type instanceof GraphQLModifiedType) {
80+
type = ((GraphQLModifiedType) type).getWrappedType();
81+
} else {
82+
return;
83+
}
84+
}
85+
}
86+
87+
private String getTypeName(GraphQLType type) {
88+
return GraphQLTypeUtil.simplePrint(type);
89+
}
90+
}

src/main/java/graphql/schema/validation/SchemaValidationErrorType.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ public enum SchemaValidationErrorType {
1717
RepetitiveElementError,
1818
InvalidDefaultValue,
1919
InvalidAppliedDirectiveArgument,
20-
InvalidAppliedDirective
20+
InvalidAppliedDirective,
21+
OutputTypeUsedInInputTypeContext,
22+
InputTypeUsedInOutputTypeContext,
2123
}

src/main/java/graphql/schema/validation/SchemaValidator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public SchemaValidator() {
2424
rules.add(new DefaultValuesAreValid());
2525
rules.add(new AppliedDirectivesAreValid());
2626
rules.add(new AppliedDirectiveArgumentsAreValid());
27+
rules.add(new InputAndOutputTypesUsedAppropriately());
2728
}
2829

2930
public List<GraphQLTypeVisitor> getRules() {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package graphql.schema.validation
2+
3+
import graphql.schema.GraphQLArgument
4+
import graphql.schema.GraphQLFieldDefinition
5+
import graphql.schema.GraphQLInputObjectType
6+
import graphql.schema.GraphQLObjectType
7+
import graphql.schema.GraphQLSchema
8+
import spock.lang.Specification
9+
10+
import static graphql.Scalars.GraphQLBoolean
11+
import static graphql.Scalars.GraphQLString
12+
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition
13+
import static graphql.schema.GraphQLInputObjectField.newInputObjectField
14+
import static graphql.schema.GraphQLInputObjectType.newInputObject
15+
import static graphql.schema.GraphQLList.list
16+
import static graphql.schema.GraphQLNonNull.nonNull
17+
import static graphql.schema.GraphQLObjectType.newObject
18+
import static graphql.schema.GraphQLTypeReference.typeRef
19+
20+
class InputAndOutputTypesUsedAppropriatelyTest extends Specification {
21+
22+
def "output type within input context is caught"() {
23+
given:
24+
25+
GraphQLObjectType OutputType = newObject()
26+
.name("OutputType")
27+
.field(newFieldDefinition().name("field").type(GraphQLString))
28+
.build()
29+
30+
GraphQLInputObjectType PersonInputType = newInputObject()
31+
.name("Person")
32+
.field(newInputObjectField()
33+
.name("friend")
34+
.type(nonNull(list(nonNull(typeRef("OutputType")))))
35+
.build())
36+
.build()
37+
38+
GraphQLFieldDefinition field = newFieldDefinition()
39+
.name("exists")
40+
.type(GraphQLBoolean)
41+
.argument(GraphQLArgument.newArgument()
42+
.name("person")
43+
.type(PersonInputType))
44+
.build()
45+
46+
GraphQLObjectType queryType = newObject()
47+
.name("Query")
48+
.field(field)
49+
.build()
50+
51+
when:
52+
GraphQLSchema.newSchema()
53+
.query(queryType)
54+
.additionalTypes([OutputType] as Set)
55+
.build()
56+
then:
57+
def schemaException = thrown(InvalidSchemaException)
58+
def errors = schemaException.getErrors().collect { it.description }
59+
errors.contains("The output type 'OutputType' has been used in an input type context : 'Person.friend'")
60+
}
61+
62+
def "input type within output context is caught"() {
63+
given:
64+
65+
GraphQLInputObjectType PersonInputType = newInputObject()
66+
.name("Person")
67+
.field(newInputObjectField()
68+
.name("friend")
69+
.type(GraphQLString)
70+
.build())
71+
.build()
72+
73+
GraphQLFieldDefinition field = newFieldDefinition()
74+
.name("outputField")
75+
.type(nonNull(list(nonNull(typeRef("Person")))))
76+
.build()
77+
78+
GraphQLObjectType queryType = newObject()
79+
.name("Query")
80+
.field(field)
81+
.build()
82+
83+
when:
84+
GraphQLSchema.newSchema()
85+
.query(queryType)
86+
.additionalTypes([PersonInputType] as Set)
87+
.build()
88+
then:
89+
def schemaException = thrown(InvalidSchemaException)
90+
def errors = schemaException.getErrors().collect { it.description }
91+
errors.contains("The input type 'Person' has been used in a output type context : 'Query.outputField'")
92+
}
93+
}

src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ class SchemaValidatorTest extends Specification {
1111
def validator = new SchemaValidator()
1212
def rules = validator.rules
1313
then:
14-
rules.size() == 6
14+
rules.size() == 7
1515
rules[0] instanceof NoUnbrokenInputCycles
1616
rules[1] instanceof TypesImplementInterfaces
1717
rules[2] instanceof TypeAndFieldRule
1818
rules[3] instanceof DefaultValuesAreValid
1919
rules[4] instanceof AppliedDirectivesAreValid
2020
rules[5] instanceof AppliedDirectiveArgumentsAreValid
21+
rules[6] instanceof InputAndOutputTypesUsedAppropriately
2122
}
2223

2324
}

0 commit comments

Comments
 (0)