Closed pmbdias closed 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();
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.
@Kirintale The current default InterfaceMappingStrategy
seems identical to what you're doing there, so you might be able to remove the customization.
Ah cool. I'll try to remove it and see what happens.
Thank you.
@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.
@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?
@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?
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.
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.
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?
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...
@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?
Try generator.withAbstractInputTypeResolution()
. That will automatically add a discriminator field and configure Jackson to use it during deserialization.
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?
@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.
I have got Java types with SPQR-Annotations the following manner:
When executing the query in spqr 0.9.6 the error occurs:
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:
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?