Skip to content

Commit 7fe7d33

Browse files
author
Bojan Tomic
committed
Ensured type references are replaced regardless of type definition order
Added a simple schema validation mechanism and a rule for recursive input types Added a test to ensure schema validation detects invalid recursive input types Added a test to ensure no dangling references exist Added a test to ensure input type references can not be used in place of output types (cherry picked from commit 7f86751)
1 parent cdedf3b commit 7fe7d33

18 files changed

+472
-21
lines changed

src/main/java/graphql/schema/GraphQLInputObjectType.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql.schema;
22

33
import java.util.ArrayList;
4+
import java.util.Collections;
45
import java.util.LinkedHashMap;
56
import java.util.List;
67
import java.util.Map;
@@ -54,6 +55,10 @@ public static Builder newInputObject() {
5455
return new Builder();
5556
}
5657

58+
public static Reference reference(String name) {
59+
return new Reference(name);
60+
}
61+
5762
@Override
5863
public GraphQLInputObjectField getFieldDefinition(String name) {
5964
return fieldMap.get(name);
@@ -128,4 +133,10 @@ public GraphQLInputObjectType build() {
128133
}
129134

130135
}
136+
137+
private static class Reference extends GraphQLInputObjectType implements TypeReference {
138+
private Reference(String name) {
139+
super(name, "", Collections.<GraphQLInputObjectField>emptyList());
140+
}
141+
}
131142
}

src/main/java/graphql/schema/GraphQLInterfaceType.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package graphql.schema;
22

3-
import graphql.AssertException;
4-
53
import java.util.ArrayList;
4+
import java.util.Collections;
65
import java.util.LinkedHashMap;
76
import java.util.List;
87
import java.util.Map;
98

9+
import graphql.AssertException;
10+
1011
import static graphql.Assert.assertNotNull;
1112

1213
public class GraphQLInterfaceType implements GraphQLType, GraphQLOutputType, GraphQLFieldsContainer, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType {
@@ -70,7 +71,10 @@ public static Builder newInterface() {
7071
return new Builder();
7172
}
7273

73-
74+
public static Reference reference(String name) {
75+
return new Reference(name);
76+
}
77+
7478
public static class Builder {
7579
private String name;
7680
private String description;
@@ -142,5 +146,9 @@ public GraphQLInterfaceType build() {
142146

143147
}
144148

145-
149+
private static class Reference extends GraphQLInterfaceType implements TypeReference {
150+
private Reference(String name) {
151+
super(name, "", Collections.<GraphQLFieldDefinition>emptyList(), new TypeResolverProxy());
152+
}
153+
}
146154
}

src/main/java/graphql/schema/GraphQLObjectType.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package graphql.schema;
22

3-
import graphql.AssertException;
4-
53
import java.util.ArrayList;
4+
import java.util.Collections;
65
import java.util.LinkedHashMap;
76
import java.util.List;
87
import java.util.Map;
98

9+
import graphql.AssertException;
10+
1011
import static graphql.Assert.assertNotNull;
1112

1213
public class GraphQLObjectType implements GraphQLType, GraphQLOutputType, GraphQLFieldsContainer, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType {
@@ -16,16 +17,27 @@ public class GraphQLObjectType implements GraphQLType, GraphQLOutputType, GraphQ
1617
private final Map<String, GraphQLFieldDefinition> fieldDefinitionsByName = new LinkedHashMap<String, GraphQLFieldDefinition>();
1718
private final List<GraphQLInterfaceType> interfaces = new ArrayList<GraphQLInterfaceType>();
1819

19-
public GraphQLObjectType(String name, String description, List<GraphQLFieldDefinition> fieldDefinitions, List<GraphQLInterfaceType> interfaces) {
20+
public GraphQLObjectType(String name, String description, List<GraphQLFieldDefinition> fieldDefinitions,
21+
List<GraphQLInterfaceType> interfaces) {
2022
assertNotNull(name, "name can't be null");
2123
assertNotNull(fieldDefinitions, "fieldDefinitions can't be null");
2224
assertNotNull(interfaces, "interfaces can't be null");
25+
assertNotNull(interfaces, "unresolvedInterfaces can't be null");
2326
this.name = name;
2427
this.description = description;
2528
this.interfaces.addAll(interfaces);
2629
buildDefinitionMap(fieldDefinitions);
2730
}
2831

32+
void replaceTypeReferences(Map<String, GraphQLType> typeMap) {
33+
for (int i = 0; i < interfaces.size(); i++) {
34+
GraphQLInterfaceType inter = interfaces.get(i);
35+
if (inter instanceof TypeReference) {
36+
this.interfaces.set(i, (GraphQLInterfaceType) new SchemaUtil().resolveTypeReference(inter, typeMap));
37+
}
38+
}
39+
}
40+
2941
private void buildDefinitionMap(List<GraphQLFieldDefinition> fieldDefinitions) {
3042
for (GraphQLFieldDefinition fieldDefinition : fieldDefinitions) {
3143
String name = fieldDefinition.getName();
@@ -35,7 +47,6 @@ private void buildDefinitionMap(List<GraphQLFieldDefinition> fieldDefinitions) {
3547
}
3648
}
3749

38-
3950
public GraphQLFieldDefinition getFieldDefinition(String name) {
4051
return fieldDefinitionsByName.get(name);
4152
}
@@ -74,7 +85,10 @@ public static Builder newObject() {
7485
return new Builder();
7586
}
7687

77-
88+
public static Reference reference(String name) {
89+
return new Reference(name);
90+
}
91+
7892
public static class Builder {
7993
private String name;
8094
private String description;
@@ -151,6 +165,11 @@ public GraphQLObjectType build() {
151165
return new GraphQLObjectType(name, description, fieldDefinitions, interfaces);
152166
}
153167

168+
}
154169

170+
private static class Reference extends GraphQLObjectType implements TypeReference {
171+
private Reference(String name) {
172+
super(name, "", Collections.<GraphQLFieldDefinition>emptyList(), Collections.<GraphQLInterfaceType>emptyList());
173+
}
155174
}
156175
}

src/main/java/graphql/schema/GraphQLSchema.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
package graphql.schema;
22

33

4+
import java.util.ArrayList;
5+
import java.util.Arrays;
6+
import java.util.Collection;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Set;
11+
412
import graphql.Assert;
513
import graphql.Directives;
6-
7-
import java.util.*;
14+
import graphql.schema.validation.InvalidSchemaException;
15+
import graphql.schema.validation.ValidationError;
16+
import graphql.schema.validation.Validator;
817

918
import static graphql.Assert.assertNotNull;
1019

@@ -99,10 +108,11 @@ public GraphQLSchema build(Set<GraphQLType> dictionary) {
99108
Assert.assertNotNull(dictionary, "dictionary can't be null");
100109
GraphQLSchema graphQLSchema = new GraphQLSchema(queryType, mutationType, dictionary);
101110
new SchemaUtil().replaceTypeReferences(graphQLSchema);
111+
Collection<ValidationError> errors = new Validator().validateSchema(graphQLSchema);
112+
if (errors.size() > 0) {
113+
throw new InvalidSchemaException(errors);
114+
}
102115
return graphQLSchema;
103116
}
104-
105-
106117
}
107-
108118
}

src/main/java/graphql/schema/GraphQLTypeReference.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* A special type to allow a object/interface types to reference itself. It's replaced with the real type
88
* object when the schema is build.
99
*/
10-
public class GraphQLTypeReference implements GraphQLType, GraphQLOutputType, GraphQLInputType {
10+
public class GraphQLTypeReference implements GraphQLType, GraphQLOutputType, GraphQLInputType, TypeReference {
1111

1212
private final String name;
1313

src/main/java/graphql/schema/GraphQLUnionType.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
import java.util.ArrayList;
55
import java.util.List;
6+
import java.util.Map;
67

78
import static graphql.Assert.*;
89

910
public class GraphQLUnionType implements GraphQLType, GraphQLOutputType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType {
1011

1112
private final String name;
1213
private final String description;
13-
private List<GraphQLObjectType> types = new ArrayList<GraphQLObjectType>();
14+
private final List<GraphQLObjectType> types = new ArrayList<GraphQLObjectType>();
1415
private final TypeResolver typeResolver;
1516

1617

@@ -25,6 +26,14 @@ public GraphQLUnionType(String name, String description, List<GraphQLObjectType>
2526
this.typeResolver = typeResolver;
2627
}
2728

29+
void replaceTypeReferences(Map<String, GraphQLType> typeMap) {
30+
for (int i = 0; i < types.size(); i++) {
31+
GraphQLObjectType type = types.get(i);
32+
if (type instanceof TypeReference) {
33+
this.types.set(i, (GraphQLObjectType) new SchemaUtil().resolveTypeReference(type, typeMap));
34+
}
35+
}
36+
}
2837

2938
public List<GraphQLObjectType> getTypes() {
3039
return new ArrayList<GraphQLObjectType>(types);
@@ -86,7 +95,5 @@ public Builder possibleTypes(GraphQLObjectType... type) {
8695
public GraphQLUnionType build() {
8796
return new GraphQLUnionType(name, description, types, typeResolver);
8897
}
89-
90-
9198
}
9299
}

src/main/java/graphql/schema/SchemaUtil.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private void collectTypesForUnions(GraphQLUnionType unionType, Map<String, Graph
6868
}
6969

7070
private void collectTypesForInterfaces(GraphQLInterfaceType interfaceType, Map<String, GraphQLType> result) {
71-
if (result.containsKey(interfaceType.getName())) return;
71+
if (result.containsKey(interfaceType.getName()) && !(result.get(interfaceType.getName()) instanceof TypeReference)) return;
7272
result.put(interfaceType.getName(), interfaceType);
7373

7474
for (GraphQLFieldDefinition fieldDefinition : interfaceType.getFieldDefinitions()) {
@@ -81,7 +81,7 @@ private void collectTypesForInterfaces(GraphQLInterfaceType interfaceType, Map<S
8181

8282

8383
private void collectTypesForObjects(GraphQLObjectType objectType, Map<String, GraphQLType> result) {
84-
if (result.containsKey(objectType.getName())) return;
84+
if (result.containsKey(objectType.getName()) && !(result.get(objectType.getName()) instanceof TypeReference)) return;
8585
result.put(objectType.getName(), objectType);
8686

8787
for (GraphQLFieldDefinition fieldDefinition : objectType.getFieldDefinitions()) {
@@ -96,7 +96,7 @@ private void collectTypesForObjects(GraphQLObjectType objectType, Map<String, Gr
9696
}
9797

9898
private void collectTypesForInputObjects(GraphQLInputObjectType objectType, Map<String, GraphQLType> result) {
99-
if (result.containsKey(objectType.getName())) return;
99+
if (result.containsKey(objectType.getName()) && !(result.get(objectType.getName()) instanceof TypeReference)) return;
100100
result.put(objectType.getName(), objectType);
101101

102102
for (GraphQLInputObjectField fieldDefinition : objectType.getFields()) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package graphql.schema;
2+
3+
public interface TypeReference {
4+
5+
String getName();
6+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package graphql.schema.validation;
2+
3+
import java.util.Collection;
4+
5+
import graphql.GraphQLException;
6+
7+
public class InvalidSchemaException extends GraphQLException {
8+
9+
private final String message;
10+
11+
public InvalidSchemaException(Collection<ValidationError> errors) {
12+
StringBuilder message = new StringBuilder("invalid schema:");
13+
for (ValidationError error : errors) {
14+
message.append("\n").append(error.getDescription());
15+
}
16+
this.message = message.toString();
17+
}
18+
19+
@Override
20+
public String getMessage() {
21+
return message;
22+
}
23+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package graphql.schema.validation;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.Set;
7+
8+
import graphql.schema.GraphQLArgument;
9+
import graphql.schema.GraphQLFieldDefinition;
10+
import graphql.schema.GraphQLInputObjectField;
11+
import graphql.schema.GraphQLInputObjectType;
12+
import graphql.schema.GraphQLInputType;
13+
import graphql.schema.GraphQLList;
14+
import graphql.schema.GraphQLModifiedType;
15+
import graphql.schema.GraphQLNonNull;
16+
import graphql.schema.GraphQLType;
17+
18+
/**
19+
* Schema validation rule ensuring no input type forms an unbroken non-nullable recursion,
20+
* as such a type would be impossible to satisfy
21+
*/
22+
public class NoUnbrokenInputCycles implements ValidationRule {
23+
24+
public void check(GraphQLFieldDefinition fieldDef, ValidationErrorCollector validationErrorCollector) {
25+
for (GraphQLArgument argument : fieldDef.getArguments()) {
26+
GraphQLInputType argumentType = argument.getType();
27+
if (argumentType instanceof GraphQLInputObjectType) {
28+
List<String> path = new ArrayList<String>();
29+
path.add(argumentType.getName());
30+
check((GraphQLInputObjectType) argumentType, new HashSet<GraphQLType>(), path, validationErrorCollector);
31+
}
32+
}
33+
}
34+
35+
private void check(GraphQLInputObjectType type, Set<GraphQLType> seen, List<String> path, ValidationErrorCollector validationErrorCollector) {
36+
if (seen.contains(type)) {
37+
validationErrorCollector.addError(new ValidationError(ValidationErrorType.UnbrokenInputCycle, getErrorMessage(path)));
38+
return;
39+
}
40+
seen.add(type);
41+
42+
for (GraphQLInputObjectField field : type.getFieldDefinitions()) {
43+
if (field.getType() instanceof GraphQLNonNull) {
44+
GraphQLType unwrapped = unwrapNonNull((GraphQLNonNull) field.getType());
45+
if (unwrapped instanceof GraphQLInputObjectType) {
46+
path = new ArrayList<String>(path);
47+
path.add(field.getName() + "!");
48+
check((GraphQLInputObjectType) unwrapped, new HashSet<GraphQLType>(seen), path, validationErrorCollector);
49+
}
50+
}
51+
}
52+
}
53+
54+
private GraphQLType unwrapNonNull(GraphQLNonNull type) {
55+
if (type.getWrappedType() instanceof GraphQLList) {
56+
//we only care about [type!]! i.e. non-null lists of non-nulls
57+
if (((GraphQLList) type.getWrappedType()).getWrappedType() instanceof GraphQLNonNull) {
58+
return unwrap(((GraphQLList) type.getWrappedType()).getWrappedType());
59+
} else {
60+
return type.getWrappedType();
61+
}
62+
} else {
63+
return unwrap(type.getWrappedType());
64+
}
65+
}
66+
67+
private GraphQLType unwrap(GraphQLType type) {
68+
if (type instanceof GraphQLModifiedType) {
69+
return unwrap(((GraphQLModifiedType) type).getWrappedType());
70+
}
71+
return type;
72+
}
73+
74+
private String getErrorMessage(List<String> path) {
75+
StringBuilder message = new StringBuilder();
76+
message.append("[");
77+
for (int i = 0; i < path.size(); i++) {
78+
if (i != 0) {
79+
message.append(".");
80+
}
81+
message.append(path.get(i));
82+
}
83+
message.append("] forms an unsatisfiable cycle");
84+
return message.toString();
85+
}
86+
}

0 commit comments

Comments
 (0)