smallrye-graphql
smallrye-graphql copied to clipboard
Define queries/mutations in a separate file
It would be nice if we could define the queries/mutations in a separate file and have the client call them using that nomenclature. For example, having the following graphql:
mutation BackportPullRequest($inputMilestone: UpdatePullRequestInput!, $inputLabel: RemoveLabelsFromLabelableInput!) {
updatePullRequest(input: $inputMilestone) {
clientMutationId
}
removeLabelsFromLabelable(input: $inputLabel) {
clientMutationId
}
}
This is how I am invoking it:
// Set Milestone and remove the backport tag
JsonObject response = graphQLClient.graphql(token, new JsonObject()
.put("query", Templates.updatePullRequest().render())
.put("variables", new JsonObject()
.put("inputMilestone", new JsonObject()
.put("pullRequestId", pullRequest.id)
.put("milestoneId", milestone.id))
.put("inputLabel", new JsonObject()
.put("labelableId", pullRequest.id)
.put("labelIds", backportLabelId)))
);
// Any errors?
if (response.getJsonArray("errors") != null) {
throw new IOException(response.toString());
}
And this is my GraphQLClient:
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.core.HttpHeaders;
import io.vertx.core.json.JsonObject;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(baseUri = "https://api.github.com/graphql")
public interface GraphQLClient {
@POST
JsonObject graphql(@HeaderParam(HttpHeaders.AUTHORIZATION) String authentication, JsonObject query);
}
I think it would be nice if you could have a, say, META-INF/graphql/MyInterface/updateFoobar.graphql
and then a MyInterface
named with a method updateFoobar(...)
to invoke it
@t1 @andymc12 @jefrajames @thejibz - what is your thoughts on this? I like it, and think it can be part of the dynamic client. w.d.y.t ?
Yes, it could definitely be a feature of the dynamic client. Taking a graphql file as input for the Query part instead of fully constructing it in Java. A question would be "does the dynamic client should parse and deserialize the graphql file into Java objects or just take it as a string ?". It raises the question of having a "2-ways" approach or not (currently, only Java to GraphQL is supported).
IMHO I don't see any value in deserializing the Graphql into Java objects, since the content will be parsed in the server side, but that's my opinion ;)
One aspect of the dynamic client is to have a complete Java representation of a GraphQL query, with programatic access to every component of it. For simple requests it might be overkill but when designing tooling or complex usecases it can be required to have such level of control.
While still experimental, the typesafe client generator already does this at compile time with an annotation processor. You use an annotation like @GraphQlQuery("removeLabel(label: String) {clientMutationId}")
and it generates a method String removeLabel(String label)
. It would be straight forward to add another annotation to get the query from a file instead of from the annotation. And it doesn't yet support mutations nor multiple requests in one query, but this works:
@GraphqlQuery("query BackportPullRequest($inputMilestone: UpdatePullRequestInput!, $inputLabel: RemoveLabelsFromLabelableInput!) {\n" +
" updatePullRequest(input: $inputMilestone) {\n" +
" clientMutationId\n" +
" }\n" +
"}")
generates a method:
@Query("updatePullRequest") String BackportPullRequest(@NonNull UpdatePullRequestInput input);
But the real challenge is the response types, i.e. the fields actually being requested. Your mutation examples seem to only return Strings. How would complex objects work, e.g. in a query {superHeroes { name realName }}
. Sure, we could return Json (array, object, or scalar), but that would introduce the technical layer into you business code again. The generator derives the return type from the schema and generates a Java class with the name coming from the schema containing only the requested fields, i.e.:
public class SuperHero {
String name;
String realName;
}
This works out quite nicely in simple cases, but I have no good solution, yet, when you have several queries for different fields of the same type.
Perhaps the generator could generate a HeroOutput
type, by analyzing the query output definition?
Mind that queries may use labeled fields, so the output might not match the fields in the types generated from the schema.
Perhaps the generator could generate a
HeroOutput
type, by analyzing the query output definition?
It still needs the schema to know which types the fields must have. And it could use the name of the query, e.g., SuperHeroesOutput
, but using the real type name is superior for clean business code.
Mind that queries may use labeled fields, so the output might not match the fields in the types generated from the schema.
I don't understand that, sorry.
I don't understand that, sorry.
This is what I mean:
{
hero {
simpleName:name
}
}
This will output:
{
"data": {
"hero": {
"simpleName": "R2-D2"
}
}
}
Thanks, now I get it. I was confused by your examples with labelable ;-)
The generator doesn't support labels, yet, but the schema is mainly required for the types, not the names.
When it comes to different queries that request the same return type, I think it's best to let the developer explicitly declare an alternative name.
I extracted the discussion about aliases (that's what the labels are called officially, see #611/#722) to #746.
This issue is about loading queries/mutations from files instead of using the text in an annotation (this is what this issue was about originally, I guess).
There are definitely other ideas worth investigating. @gastaldi : do you use the generator already? I prefer to work on practical problems from real projects, so your input would be very helpful!
@t1 hey! no, I am not using the generator.