leangen / graphql-spqr

Build a GraphQL service in seconds
Apache License 2.0
1.1k stars 182 forks source link

GraphQL-Interfaces error with SPQR-Annotations #282

Closed pmbdias closed 5 years ago

pmbdias commented 5 years ago

I have got Java types with SPQR-Annotations the following manner:

@GraphQLInterface(name = "IAnimal", implementationAutoDiscovery=true)
public interface IAnimal {
    @GraphQLQuery(name = "name")
    public String getName();
}

@GraphQLType(name = "IBat")
@GraphQLTypeResolver(DummyTypeResolver.class) 
public interface IBat extends IAnimal {
    @GraphQLQuery(name = "wingSpan")
    public int getWingSpan();
}

@GraphQLType(name = "ICat")
@GraphQLTypeResolver(DummyTypeResolver.class) 
public interface ICat extends IAnimal {
    @GraphQLQuery(name = "smart")
    public boolean isSmart();
}

@GraphQLType(name = "ICow")
@GraphQLTypeResolver(DummyTypeResolver.class) 
public interface ICow extends IAnimal {
    @GraphQLQuery(name = "leiteira")
    public boolean isLeiteira();
}

@GraphQLType(name = "Dog")
@GraphQLTypeResolver(DummyTypeResolver.class) 
public class DogImpl implements IAnimal{

    @Override
    @GraphQLQuery(name = "name")
    public String getName() {       
        return "dog";
    }

    @GraphQLQuery(name = "color")
    public String getColor() {      
        return "black";
    }

}
@Service
public class ServiceAnimal {
    @GraphQLQuery(name = "zoo")
    public List<IAnimal> getAnimals() {
        List<IAnimal> retval = new ArrayList<>();
        retval.add(new BatImpl());
        retval.add(new CatImpl());
        retval.add(new CowImpl());
        return retval;
    }

    @GraphQLQuery(name = "zoocomplet")
    public List<IAnimal> getAnimalsCompleto() {
        List<IAnimal> retval = new ArrayList<>();
        retval.add(new BatImpl());
        retval.add(new CatImpl());
        retval.add(new CowImpl());
        retval.add(new DogImpl());
        return retval;
    }

    @GraphQLQuery(name = "cat")
    public ICat getCat() {
        return new CatImpl();
    }

    @GraphQLQuery(name = "bat")
    public IBat getBat() {
        return new BatImpl();
    }

    @GraphQLQuery(name = "cow")
    public ICow getCow() {
        return new CowImpl();
    }

    @GraphQLQuery(name = "dog")
    public IAnimal getDog() {
        return new DogImpl();
    }
}

When executing the query in spqr 0.9.6 the error occurs:

{"query":"{__type(name:\"IAnimal\"){name, possibleTypes { name } }}"}
{data={__type={name=IAnimal, possibleTypes=[{name=IBat}, {name=ICat}, {name=ICow}]}}}
---------------------
{"query":"{zoo{name, ... on ICat { smart }, ... on IBat {wingSpan}, ... on ICow {leiteira}}}"}
{data={zoo=[{name=bat, wingSpan=18}, {name=cat, smart=false}, {name=cow, leiteira=true}]}}
---------------------
{"query":"{zoocomplet{name, ... on ICat { smart }, ... on IBat {wingSpan}, ... on ICow {leiteira}, ... on Dog{color}}}"}
{errors=[{message=Validation error of type UnknownType: Unknown type Dog, locations=[]}]}

The Dog type does not appear in the list of possible types. The query that does not return the Dog type works correctly, but the query that has the Dog object occurs the unknown type error.

In the spqr 0.9.9:

{"query":"{__type(name:\"IAnimal\"){name, possibleTypes { name } }}"}
{data={__type={name=IAnimal, possibleTypes=[{name=Dog}, {name=ICow}, {name=IBat}, {name=ICat}]}}}
---------------------
{"query":"{zoo{name, ... on ICat { smart }, ... on IBat {wingSpan}, ... on ICow {leiteira}}}"}
{data={zoo=[null, null, null]}, errors=[{message=Can't resolve '/zoo[0]'. Abstract type 'IAnimal' must resolve to an Object type at runtime for field 'null.zoo'. Runtime Object type 'BatImpl' is not a possible type for 'IAnimal'., path=[zoo, 0]}, {message=Can't resolve '/zoo[1]'. Abstract type 'IAnimal' must resolve to an Object type at runtime for field 'null.zoo'. Runtime Object type 'CatImpl' is not a possible type for 'IAnimal'., path=[zoo, 1]}, {message=Can't resolve '/zoo[2]'. Abstract type 'IAnimal' must resolve to an Object type at runtime for field 'null.zoo'. Runtime Object type 'CowImpl' is not a possible type for 'IAnimal'., path=[zoo, 2]}]}
---------------------
{"query":"{zoocompleto{name, ... on ICat { smart }, ... on IBat {wingSpan}, ... on ICow {leiteira}, ... on Dog {cor}}}"}
{data={zoocompleto=[null, null, null, {name=dog, cor=black}]}, errors=[{message=Can't resolve '/zoocompleto[0]'. Abstract type 'IAnimal' must resolve to an Object type at runtime for field 'null.zoocompleto'. Runtime Object type 'BatImpl' is not a possible type for 'IAnimal'., path=[zoocompleto, 0]}, {message=Can't resolve '/zoocompleto[1]'. Abstract type 'IAnimal' must resolve to an Object type at runtime for field 'null.zoocompleto'. Runtime Object type 'CatImpl' is not a possible type for 'IAnimal'., path=[zoocompleto, 1]}, {message=Can't resolve '/zoocompleto[2]'. Abstract type 'IAnimal' must resolve to an Object type at runtime for field 'null.zoocompleto'. Runtime Object type 'CowImpl' is not a possible type for 'IAnimal'., path=[zoocompleto, 2]}]}

Here the Dog type does appear in the list of possible types. But the ICat, IBat, and ICow types are returned as null and the Dog type is returned the data correctly.

So there is a different behavior in spqr versions. Any idea what might be happening?

Kirintale commented 5 years ago

What does your schema generator look like?

I currently have the follow and it seems to work for me (though our code base is different). Hope this helps.

final GraphQLSchema schema = new GraphQLSchemaGenerator()
                .withInterfaceMappingStrategy(new InterfaceMappingStrategy() {
                    @Override
                    public boolean supports(final AnnotatedType interfase) {
                        return interfase.isAnnotationPresent(GraphQLInterface.class);
                    }

                    @Override
                    public Collection<AnnotatedType> getInterfaces(final AnnotatedType type) {
                        @SuppressWarnings("rawtypes")
                        Class clazz = ClassUtils.getRawType(type.getType());
                        final Set<AnnotatedType> interfaces = new HashSet<>();
                        do {
                            final AnnotatedType currentType = GenericTypeReflector.getExactSuperType(type, clazz);
                            if (supports(currentType)) {
                                interfaces.add(currentType);
                            }
                            Arrays.stream(clazz.getInterfaces())
                                    .map(inter -> GenericTypeReflector.getExactSuperType(type, inter))
                                    .filter(this::supports).forEach(interfaces::add);
                        } while ((clazz = clazz.getSuperclass()) != Object.class && clazz != null);
                        return interfaces;
                    }
                }).withOperationsFromSingletons(service)// register the service
                .generate(); // done ;)

        graphQL = new GraphQL.Builder(schema).build();
kaqqao commented 5 years ago

You have a rather complicated and confusing setup here, as you sometimes use classes and sometimes interfaces as concrete implementations. Still, there's nothing strictly wrong with it, it's just difficult to untangle the expected behavior.

Now, where it gets strange is implementationAutoDiscovery. What it does, when enabled, is scan the classpath for concrete implementations, and registers them with the schema. Additionally, all the discovered implementation are tracked for automatic type resolution. This in your case means CatImpl, DogImpl, BatImpl and CowImpl are discovered and added. Now, because you also have queries that directly expose ICat, ICow and IBat, those get added to the schema and tracked as implementations as well. What this means is that both e.g. ICat (mapped as Cat GraphQL type) and CatImpl (mapped as CatImpl GraphQL type) get registered as possible implementations of IAnimal and the default TypeResolver (DelegatingTypeResolver) will check the returned instance as try to uniquely match it to a known subtype. So when you return a CatImpl, 2 GraphQL types match, so it looks for a custom TypeResolver to decide. It looks on the current type (CatImpl) and the interface type (IAnimal), but in your case, it doesn't find it anywhere, because the annotation is on ICat. So it checks if there's a type directly matching the instance type (CatImpl) and it finds a match, so it returns the CatImpl GraphQL type as the result. Now, there's a limitation/bug in SPQR that only the directly implemented interfaces get mapped. So for CatImpl, only ICat is analyzed, it doesn't match (because it's mapped as an object type), so no GraphQL interfaces get mapped. As a result, you get an explosion - because CatImpl is the detected implementation, but in the schema CatImpl doesn't implement IAnimal.

As you see, this is super-convoluted... your best option is to simply disable implementationAutoDiscovery as you don't actually need to track Impl classes, as you already directly expose the implementations. You might also want to move @GraphQLTypeResolver(DummyTypeResolver.class) to IAnimal... You could also provide your own InterfaceMappingStrategy that deals with indirect interfaces.

And I'm investigating fixing the only direct interfaces issue, so that should be dealt with soon as well.

kaqqao commented 5 years ago

@Kirintale The current default InterfaceMappingStrategy seems identical to what you're doing there, so you might be able to remove the customization.

Kirintale commented 5 years ago

Ah cool. I'll try to remove it and see what happens.

Thank you.

kaqqao commented 5 years ago

@pmbdias I've pushed a commit fixing the bug I explained above (where only direct interfaces would be taken into account). Now all interfaces should be mapped as expected.

@Kirintale As a result of addressing the bug discussed here, the default InterfaceMappingStrategy has changed a bit, to also go into all transitive interfaces, so the logic now differs slightly from yours.

pmbdias commented 5 years ago

@kaqqao Thank you for your interest in helping me. I have managed to solve the interface problem when the object is in the same project as the schema generator. However, I am trying to apply graphql to an old project, it is a monolithic system that is being broken into smaller ear's and so I am having more difficulties. I have a graphql project that generates the schema and I will call it main. However I have interfaces annotated with graphl in another jar that is declared as maven dependency of the main graphlql project. These interfaces declared in this other jar are not being mapped in the schema. For the interfaces declared in the main graphql project the mapping is correct and working. Is there a way to map in the graphql schema the interfaces (@GraphQLInterface) that are declared in another jar?

pmbdias commented 5 years ago

@kaqqao I posted on github a project for you to better understand the problem I described in the post above. https://github.com/pmbdias/graphql When objects are in the same project that generates the schema, attributes that are of type interface are recognized. However, when objects come as a dependency on another project, the interfaces are not mapped, the other concrete types are. Run the org.mountcloud.graphql.GraphqlClientMain from the graphql-client-master project to view both the working and non-working cases. Do you have any idea what might be happening?

kaqqao commented 5 years ago

This is almost certainly a classloading issue. Do you have 2 SPQR jars on your classpath? E.g. once in your EAR and once in your WAR? I've seen this happen before. The annotation would be loaded twice, by different classloaders, so they wouldn't be equal to each other when compared.

kaqqao commented 5 years ago

I can neither figure out how to start the web application that the org.mountcloud.graphql.GraphqlClientMain expects to exist nor what the expected output should be. But, since I'm 99% sure this is a classloading issue, try removing this dependency. The web project already depends on the EJB project which depends on SPQR, so no need to declare it again. If you double-declare your dependencies, you run exactly into this type of problem as different classloaders are used to load the annotations and the service classes.

pmbdias commented 5 years ago

The reported problem was not the existence of 2 SPQR jars on my classpath. But it was classloading problem. I was packing SPQR jar in war, so when I moved it to lib from ear it solved the problem. Thank you very much.

You have any indication of java client for consumption of queries and graphql mutations?

I am looking for a client so that other applications can consume my graphql application. I found the link (https://github.com/MountCloud/graphql-client ) but realized missing treatment when you want to pass parameter like GraphQLInputObjectType. Example: input: {email: "david@email.com", firstName: "David"}

Do you have any sugestion?

kaqqao commented 5 years ago

In EAR packages, multiple classloaders are used to load different deployment units. And if the packaging isn't done exactly right, you can end up in your situation where a class (the @GraphQLInterface annotation in your case) apparently does not match itself.

As for the client, I'm not aware of anything particularly good. Apollo Android is maybe the easiest to use. There's also AmericanExpress' Nodes GraphQL client but I've personally never a use-case matching theirs. Shopify has a very cool Java client generator but it needs Ruby for generating the code. Satisfying this via JRuby JAR could be an interesting approach. Someone also made a simple SPQR-aware client but I can't seem to find it right now...

pmbdias commented 5 years ago

@kaqqao I have one more difficulty now regarding mutation. I don't know passing complex parameter which has interface type attribute. How to define in mutation what is the implementation of the interface I want to pass. Example:

{"query":"mutation{addPerson(person:{name:\"John\",identification:{identification:\"02674859902\"}}){name}}"}

I have the Person object that has two attributes: name and identification. But the identification attribute is type IdentificationIf which has two implementations IdentificationCPF and IdentificationCNPJ. In the query I can get the right result using spred:
{"query":"query{person{name, identification{... on IdentificationCPF{identification}}}}"}

I need to call a mutation passing the Person object but I don't know how to enter the identification due to the type being an Interface. Can you help me?

kaqqao commented 5 years ago

Try generator.withAbstractInputTypeResolution(). That will automatically add a discriminator field and configure Jackson to use it during deserialization.

pmbdias commented 5 years ago

E.g. given the interface IdentificacaoIf, if two implementation types are discovered IdentificacaoCPF and IdentificacaoCNPJ What is the mutation syntax to indicate which concrete object will be passed? When I mutate this way: "mutation{addPerson(person:{name:\"John\",identification:{identification:\"02674859902\"}}){name}}" This error occurs: "Validation error of type WrongType: argument 'person.identification' with value 'ObjectValue{objectFields=[ObjectField{name='name', value=StringValue{value='John'}}, ObjectField{name='identification', value=ObjectValue{objectFields=[ObjectField{name='identification', value=StringValue{value='02674859902'}}, ObjectField{name='statusIdentCPF', value=NullValue{}}]}}]}' contains a field not in 'IdentificationIfInput': 'identification' @ 'addPerson'"

I understand that this way there is no way for graphql to know which object it needs to instantiate. But I don't know how to indicate the concrete object in the mutation. Could you show an example of how to make the mutation call?

KMUS commented 4 years ago

@pmbdias @kaqqao

I have exactly the same problem here and have no idea how to solve it.

@Data
@GraphQLInterface(name = "AssetChild", implementationAutoDiscovery = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "_type_")
@JsonSubTypes( {
        @JsonSubTypes.Type(value = BuildingChild.class, name = "BuildingChild")
    }
)
public abstract class AssetChild {
  String s1;
}

@Data
@Dependent
@EqualsAndHashCode(callSuper = true)
public class BuildingChild extends AssetChild {
  String s2;
}

@GraphQLMutation
public AssetChild save(AssetChild input) {
  return input;
}

mutation {
  save(input: {
    _type_:BuildingChild
    s1: "s1-val"
  }) {
   s1
    __typename
  }
}

This works. But its typ is BuildingChild and I want to set s2 too which is not possible. Use SPQR 0.10.1 and Quarkus here. Thanks in advance.