Skip to content
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

Xuorig Fix PR - Edge case with GraphQLTypeReference and Schema Transforms #2906

Merged
merged 7 commits into from Aug 11, 2022
18 changes: 11 additions & 7 deletions src/main/java/graphql/schema/GraphQLTypeUtil.java
Expand Up @@ -196,12 +196,7 @@ public static <T extends GraphQLType> T unwrapOneAs(GraphQLType type) {
* @return the underlying type
*/
public static GraphQLUnmodifiedType unwrapAll(GraphQLType type) {
while (true) {
if (isNotWrapped(type)) {
return (GraphQLUnmodifiedType) type;
}
type = unwrapOne(type);
}
return unwrapAllAs(type);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was probably a mistake to have the well named unwrapAll return GraphQLUnmodifiedType which excludes GraphqlTypeReferences - but API is API so leave it as is

I did change how it works internally here

}

/**
Expand All @@ -215,7 +210,16 @@ public static GraphQLUnmodifiedType unwrapAll(GraphQLType type) {
*/
public static <T extends GraphQLType> T unwrapAllAs(GraphQLType type) {
//noinspection unchecked
return (T) unwrapAll(type);
return (T) unwrapAllImpl(type);
}

private static GraphQLType unwrapAllImpl(GraphQLType type) {
while (true) {
if (isNotWrapped(type)) {
return type;
}
type = unwrapOne(type);
}
}


Expand Down
Expand Up @@ -22,12 +22,16 @@
import java.util.Map;
import java.util.TreeMap;

import static graphql.schema.GraphQLTypeUtil.isNotWrapped;
import static graphql.schema.GraphQLTypeUtil.unwrapAllAs;
import static graphql.schema.GraphQLTypeUtil.unwrapOne;
import static java.lang.String.format;

@Internal
public class GraphQLTypeCollectingVisitor extends GraphQLTypeVisitorStub {

private final Map<String, GraphQLNamedType> result = new LinkedHashMap<>();
private final Map<String, GraphQLNamedType> fieldActualTypes = new LinkedHashMap<>();

public GraphQLTypeCollectingVisitor() {
}
Expand Down Expand Up @@ -86,10 +90,14 @@ public TraversalControl visitGraphQLUnionType(GraphQLUnionType node, TraverserCo

@Override
public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext<GraphQLSchemaElement> context) {

GraphQLNamedType type = unwrapAllAs(node.getType());
if (! (type instanceof GraphQLTypeReference)) {
fieldActualTypes.put(type.getName(), type);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only puts in fields that unwrap to be an actual type

return super.visitGraphQLFieldDefinition(node, context);
}


private boolean isNotTypeReference(String name) {
return result.containsKey(name) && !(result.get(name) instanceof GraphQLTypeReference);
}
Expand All @@ -113,17 +121,38 @@ private void assertTypeUniqueness(GraphQLNamedType type, Map<String, GraphQLName
if (existingType != null) {
// type references are ok
if (!(existingType instanceof GraphQLTypeReference || type instanceof GraphQLTypeReference))
// object comparison here is deliberate
// object comparison here is deliberate
{
if (existingType != type) {
throw new AssertException(format("All types within a GraphQL schema must have unique names. No two provided types may have the same name.\n" +
"No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types).\n" +
"You have redefined the type '%s' from being a '%s' to a '%s'",
type.getName(), existingType.getClass().getSimpleName(), type.getClass().getSimpleName()));
}
}
}
}

public ImmutableMap<String, GraphQLNamedType> getResult() {
return ImmutableMap.copyOf(new TreeMap<>(result));
Map<String, GraphQLNamedType> types = new TreeMap<>(fixFieldDanglingTypes(result));
return ImmutableMap.copyOf(types);
}

/**
* It's possible for certain field edits to create a situation where a field had a type reference, then
* it got replaced with an actual reference and then the schema gets edited such that the only reference
* to that type is the replaced field reference. This edge case means that the replaced reference can be
* missed if it's the only way to get to that type.
*
* @param visitedTypes the types collected by this visitor
*
* @return a fixed up map where the only
*/
private Map<String, GraphQLNamedType> fixFieldDanglingTypes(Map<String, GraphQLNamedType> visitedTypes) {
for (GraphQLNamedType fieldPointerType : fieldActualTypes.values()) {
String typeName = fieldPointerType.getName();
visitedTypes.putIfAbsent(typeName, fieldPointerType);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we have a type pointed to by a field AND it's not the the list of visited types, we add it since the field is the only pointer to it say

}
return visitedTypes;
}
}
124 changes: 123 additions & 1 deletion src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy
Expand Up @@ -7,9 +7,12 @@ import graphql.GraphQL
import graphql.TestUtil
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.TypeRuntimeWiring
import graphql.util.TraversalControl
import graphql.util.TraverserContext
import spock.lang.Specification

import java.util.function.UnaryOperator
import java.util.stream.Collectors

import static graphql.Scalars.GraphQLString
import static graphql.StarWarsSchema.characterInterface
Expand All @@ -18,6 +21,7 @@ import static graphql.StarWarsSchema.humanType
import static graphql.StarWarsSchema.starWarsSchema
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition
import static graphql.schema.GraphQLObjectType.newObject
import static graphql.schema.GraphQLTypeReference.typeRef

class GraphQLSchemaTest extends Specification {

Expand Down Expand Up @@ -187,7 +191,6 @@ class GraphQLSchemaTest extends Specification {
type Dog implements Pet {
name : String
}

type Cat implements Pet {
name : String
}
Expand All @@ -214,6 +217,124 @@ class GraphQLSchemaTest extends Specification {
dogType.getName() == "Dog"
}

def "issue with type references when original type is transformed away"() {
def sdl = '''
type Query {
# The b fields leads to the addition of the B type (actual definition)
b: B
# When we filter out the `b` field, we can still access B through A
# however they are GraphQLTypeReferences and not an actual GraphQL Object
a: A
}

type A {
b: B
}

type B {
a: A
b: B
}
'''

when:
def schema = TestUtil.schema(sdl)
// When field `a` is filtered out
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b is filtered out

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

List<GraphQLFieldDefinition> fields = schema.queryType.fieldDefinitions.stream().filter({
it.name == "a"
}).collect(Collectors.toList())
// And we transform the schema's query root, the schema building
// will throw because type B won't be in the type map anymore, since
// there are no more actual B object types in the schema tree.
def transformed = schema.transform({
it.query(schema.queryType.transform({
it.replaceFields(fields)
}))
})

then:
transformed.containsType("B")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Andres original test that was failing

}

def "can change types via SchemaTransformer and visitor"() {
def sdl = '''
type Query {
# The b fields leads to the addition of the B type (actual definition)
b: B
# When we filter out the `b` field, we can still access B through A
# however they are GraphQLTypeReferences and not an actual GraphQL Object
a: A
}

type A {
b: B
}

type B {
a: A
b: B
}
'''

when:
def schema = TestUtil.schema(sdl)

GraphQLTypeVisitorStub visitor = new GraphQLTypeVisitorStub() {
@Override
TraversalControl visitGraphQLObjectType(GraphQLObjectType objectType, TraverserContext<GraphQLSchemaElement> context) {
GraphQLCodeRegistry.Builder codeRegistry = context.getVarFromParents(GraphQLCodeRegistry.Builder.class);
// we need to change __XXX introspection types to have directive extensions
if (objectType.getName().equals("Query")) {
def queryType = objectType
List<GraphQLFieldDefinition> fields = queryType.fieldDefinitions.stream().filter({
it.name == "a"
}).collect(Collectors.toList())

GraphQLObjectType newObjectType = queryType.transform({
it.replaceFields(fields)
})

return changeNode(context, newObjectType);
}
return TraversalControl.CONTINUE
}
}
GraphQLSchema transformed = SchemaTransformer.transformSchema(schema, visitor)

then:
transformed.containsType("B")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My SchemaTransformer test that was failing and now passing

}

def "fields edited from type references should still built valid schemas"() {
def typeB = newObject().name("B")
.field(newFieldDefinition().name("a").type(typeRef("A")))
.field(newFieldDefinition().name("b").type(typeRef("B")))
.build()


def typeAFieldB = newFieldDefinition().name("b").type(typeRef("B")).build()
// at the line above typeB is never strongly referenced
// and this simulates an edit situation that wont happen with direct java declaration
// but where the type reference is replaced to an actual but its the ONLY direct reference
// to that type``
typeAFieldB.replaceType(typeB)

def typeA = newObject().name("A")
.field(typeAFieldB)
.build()

def queryType = newObject().name("Query")
.field(newFieldDefinition().name("a").type(typeA))
.build()

when:
def schema = GraphQLSchema.newSchema().query(queryType).build()
then:
schema.getType("A") != null
schema.getType("B") != null
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct object test case (no SDL) that was failing and is now passing


def "cheap transform without types transformation works"() {

def sdl = '''
Expand Down Expand Up @@ -324,4 +445,5 @@ class GraphQLSchemaTest extends Specification {
thrown(AssertException)

}

}
33 changes: 33 additions & 0 deletions src/test/groovy/graphql/schema/GraphQLTypeUtilTest.groovy
Expand Up @@ -6,6 +6,7 @@ import static graphql.Scalars.GraphQLString
import static graphql.schema.GraphQLList.list
import static graphql.schema.GraphQLNonNull.nonNull
import static graphql.schema.GraphQLObjectType.newObject
import static graphql.schema.GraphQLTypeReference.*

class GraphQLTypeUtilTest extends Specification {

Expand Down Expand Up @@ -127,6 +128,38 @@ class GraphQLTypeUtilTest extends Specification {

}

def "unwrapAllAs tests"() {
def type = list(nonNull(list(nonNull(GraphQLString))))
def typeRef = list(nonNull(list(nonNull(typeRef("A")))))

when:
type = GraphQLTypeUtil.unwrapAllAs(type)

then:
type == GraphQLString

when:
type = GraphQLTypeUtil.unwrapAllAs(type)

then:
type == GraphQLString

when:
typeRef = GraphQLTypeUtil.unwrapAllAs(typeRef)

then:
typeRef instanceof GraphQLTypeReference
(typeRef as GraphQLTypeReference).name == "A"

when:
typeRef = GraphQLTypeUtil.unwrapAllAs(typeRef)

then:
typeRef instanceof GraphQLTypeReference
(typeRef as GraphQLTypeReference).name == "A"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra tests as unwrapAllAs was not previously covered

}

def "isLeaf tests"() {
when:
def type = GraphQLString
Expand Down