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

BUG: The DelegatingTypeResolver breaks when updating the GraphQLSchema using a Visitor #490

Open
Balf opened this issue Mar 26, 2024 · 1 comment

Comments

@Balf
Copy link
Collaborator

Balf commented Mar 26, 2024

Hi @kaqqao,

I found an interesting bug within SPQR concerning the combinations of Unions and visitors. I've got an Java interface in GraphQL which is purely used to indicate that any classes implementing the interface should be part of a Union, as the types do not share fields.

The issue is as follows: When creating a Union Type using the @GraphQLUnion annotation the union is created as expected. When creating a query that returns that Union Type the fields on members of that Union can be requested as normal. The fun begins when a Visitor is used to mutate one of the members of the Union: After the member has been changed the member does not return any data when queried, just an empty Map.

I've compared this to a plain GraphQL Java implementation where I've build my own field definitions and runtimewiring etc. When I mutate this schema using the same Visitor it still works as expected, whereas the schema generated with SPQR breaks.

I've created a code sample that demonstrates the issue. The full code is attached, but below I'll list the most relevant parts.

@GraphQLUnion(name = "Item", possibleTypeAutoDiscovery = true)
public interface Item {

}

@GraphQLType
public class Dog implements Item {
    private String name = "Bello";

    private int paws = 4;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    // Ignoring getPaws here for demonstration's sake.
    @GraphQLIgnore
    public int getPaws() {
        return paws;
    }

    public void setPaws(int paws) {
        this.paws = paws;
    }
}

@GraphQLType
public class Car implements Item {
    private int numberOfWheels = 4;

    public int getNumberOfWheels() {
        return numberOfWheels;
    }

    public void setNumberOfWheels(int numberOfWheels) {
        this.numberOfWheels = numberOfWheels;
    }
}


public class DataService {

  @GraphQLQuery
  public Dog getDog() {
      return new Dog();
  }

  @GraphQLQuery
  public Car getCar() {
      return new Car();
  }

  @GraphQLQuery
  public List<Item> getItems() {
      return List.of(new Dog(), new Car());
  }
}

//Now to build the schema and execute a query against it

ExecutionInput executionInput = ExecutionInput
    .newExecutionInput()
    .query("query { items { ...on Dog { name } ... on Car { numberOfWheels }} }")
    .build();

GraphQLSchemaGenerator generator = new GraphQLSchemaGenerator();
generator.withOperationsFromSingleton(new DataService());
GraphQLSchema spqrSchema = generator.generate();
GraphQL spqr = GraphQL.newGraphQL(spqrSchema).build();

ExecutionResult spqrResult = spqr.execute(executionInput);

// Here the spqrResult will correctly return a Dog object with a name field containing Bello and 
// a Car object with a numberOfWheels field containing 4.

// Now let's mutate the schema using the following Visitor
private GraphQLTypeVisitorStub getGraphQLTypeVisitorStub() {
    return new GraphQLTypeVisitorStub() {
        @Override
        public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
            if (node.getName().equals("Dog")) {
                return changeNode(context, node.transform(builder -> builder
                        .field(GraphQLFieldDefinition.newFieldDefinition()
                                .name("paws")
                                .type(GraphQLInt)
                                .build()
                        ).build()
                ));
            }
            return TraversalControl.CONTINUE;
        }
    };
}

spqrSchema = SchemaTransformer.transform(spqrSchema, getGraphQLTypeVisitorStub);        

// And repeat the query

ExecutionResult spqrResultAfterVisitor = spqr.execute(executionInput);
// spqrResultAfterVisitor now contains an empty LinkedHashMap in the data field, see attached screenshots
Schema_not_visited

The ExecutionResult when the Schema has not been visisted

Schema_visited

The result when the Schema has been visited

The attached ZIP file contains a Java file, containing an HTTP Servlet, that generates a Schema based on both GraphQL SPQR and based on plain GraphQL Java, using the same classes. The line that transforms the SPQR generated schema is commented out, so in it's current state both calls return the correct data. You can uncomment line 57 to transform the spqr schema and see the same results I see in the screenshots above.

I've looked into the issue a bit myself: it seems that the Dog object is actually retrieved in both situations, it just the translation back to the API that seems to be broken.
SimpleGraphQLServlet.java.zip

@Balf Balf changed the title BUG: Using a Visitor on a member of GraphQLUnion defined via SPQR breaks the visisted member within the union BUG: Using a Visitor on a member of GraphQLUnion defined via SPQR breaks the visited member within the union Mar 26, 2024
@Balf
Copy link
Collaborator Author

Balf commented Mar 26, 2024

Something I initially forgot to mention: This issue only occurs when querying the items query. When directly querying the dog query it works as expected. I've looked in it a bit further and I think that the issue is strongly related to the DelegatingTypeResolver.

 //Check if the type is already unambiguous
  List<MappedType> mappedTypes = globalEnv.typeRegistry.getOutputTypes(abstractTypeName, resultType);
  if (mappedTypes.isEmpty()) {
      return (GraphQLObjectType) env.getSchema().getType(resultTypeName);
  }
  if (mappedTypes.size() == 1) {
      return mappedTypes.get(0).getAsObjectType();
  }

In my example the code reaches the line:

return mappedTypes.get(0).getAsObjectType();

When I debug the result of mappedTypes.get(0).getAsObjectType() I see that the paws field is missing from the FieldDefinition. I compared this to the UnionTypeResolver in the attached Java file, and I see that the field is present there.

So it seems that the globalEnv in the DelegatingTypeResolver is not properly updated when a Visitor is used.

@Balf Balf changed the title BUG: Using a Visitor on a member of GraphQLUnion defined via SPQR breaks the visited member within the union BUG: The DelegatingTypeResolver breaks when updating the GraphQLSchema using a Visitor Mar 27, 2024
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

No branches or pull requests

1 participant