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

Conversation

bbakerman
Copy link
Member

This is the same code as #2905 but with an extra test for SchemaTransformer which also fails

@bbakerman
Copy link
Member Author

I debugged this further. The reason this blows up (even for SchemaTransformer changes along with direct ones) is this

The first step of the schema build code calls this

            SchemaUtil.visitPartiallySchema(partiallyBuiltSchema, codeRegistryVisitor, typeCollectingVisitor);
            ImmutableMap<String, GraphQLNamedType> allTypes = typeCollectingVisitor.getResult();

At this point the map is meant to contain a map of name -> ACTUAL types (and not type references)

Later the type reference replacement code finds all type references and expects to find the actual type for them in the map above and its does not and asserts.

However this particular edit means that the schema DOES have a field pointer to a actual type but the GraphQLTypeCollectingVisitor never grabs it because it only visits types via direct declaration and not via field declaration.

This makes sense in SDL generation because its impossible to create a situation where the actual type is not declared but code declaration can make this happen as demonstrated in this PR test case.

@bbakerman
Copy link
Member Author

Ok debugging more - this is more about the Traverser related code

In this "edit case" we have a field b that at some point earlier had a type reference to B in the field def. On the original schema build this gets resolved to an actual type. So now the field reference has originalType=typeRef("B") and also replacedType=actualTypeB

Then the code does and edit and this gets rid of the direct reference to type B (which came via another field chain) but this field b still has originalType=typeRef("B") andreplacedType=actualTypeB.

So Type B is still around in some form

The graphql.schema.impl.GraphQLTypeCollectingVisitor#GraphQLTypeCollectingVisitor setup code does this

        traverser = new SchemaTraverser(schemaElement -> schemaElement.getChildrenWithTypeReferences().getChildrenAsList());

and the callback to the GraphqlFieldDefinition is getChildrenWithTypeReferences which is code as

        return SchemaElementChildrenContainer.newSchemaElementChildrenContainer()
                .child(CHILD_TYPE, originalType)
                .children(CHILD_ARGUMENTS, arguments)
                .children(CHILD_DIRECTIVES, directivesHolder.getDirectives())
                .children(CHILD_APPLIED_DIRECTIVES, directivesHolder.getAppliedDirectives())
                .build();

So while the replace type is in place - its never used.

The question is - should it be?

@bbakerman
Copy link
Member Author

The code rightly wants a DAG - if we change it, it starts to fail with

Internal error: should never happen: This schema is not forming an Directed Acyclic Graph

The challenge here is this. The edit caused the field to be the only reference to the type B BUT the traverser in the schema build uses getChildrenWithTypeReferences and this missed this type.

It's as if the specific use case GraphQLTypeCollectingVisitor needs to grab dangling field references and do a fix up if they are missing - eg type referenced but not in the schema in some other way

}
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

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

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

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()));
Copy link
Member Author

Choose a reason for hiding this comment

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

made a common method here


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

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

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

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

@bbakerman bbakerman changed the title Xuorig repro PR - Edge case with GraphQLTypeReference and Schema Transforms Xuorig Fix PR - Edge case with GraphQLTypeReference and Schema Transforms Aug 2, 2022

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

@bbakerman bbakerman added this to the 19.1 milestone Aug 2, 2022
}

@Override
public TraversalControl visitGraphQLScalarType(GraphQLScalarType node, TraverserContext<GraphQLSchemaElement> context) {
assertTypeUniqueness(node, result);
save(node.getName(), node);
return super.visitGraphQLScalarType(node, context);
return CONTINUE;
Copy link
Member Author

Choose a reason for hiding this comment

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

all of this super code boiled down to CONTINUE!

Copy link

@xuorig xuorig left a comment

Choose a reason for hiding this comment

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

Appreciate the fix! Very similar solution to the workaround I implemented but much much nicer now that it is built in in the type collector. Thanks 🙇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants