Skip to content

A GraphQL Client in Java (inspired by JAX-RS Client)

Notifications You must be signed in to change notification settings

worldline/dynaql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GraphQL dynamic Client for MicroProfile

Rationale

MicroProfile GraphQL 1.0 has been focused on the server-side enabling to develop and expose GraphQL endpoints. The purpose of this specification is to define a so-called "dynamic" client API.

"Dynamic" means close to the GraphQL specification semantic and structure. We can compare it with what JAX-RS client API is for REST.

A "type-safe" client API, similar to MicroProfile RestClient, should also be proposed in another document.

Goals

  1. Provides full control over the MicroProfile GraphQL capabilities: operations, arguments, variables, input types, scalars, interface, partial results, errors …​

  2. Consistency with MicroProfile GraphQL server annotations (DateFormat, NumberFormat …​) and capabilities

  3. Consistency with MicroProfile:

    • No dependency outside MicroProfile core

    • Configuration exclusively based on MicroProfile Config

    • Support of JSON-B format directives

Non goals

  1. Transport layer support: the GraphQL specification is independent of transport layer. We propose to stay aligned with this, leaving the final implementation free to use any client network library (JAX-RS, Apache HTTPClient …​).

  2. Serialization of custom Java classes to build requests

  3. Deserialization of responses into custom Java classes

Focus of release 1.1

A first version of the client API is planned with MicroProfile GraphQL 1.1.

For this first step, we propose to focus on the following core features.

GraphQL components support

bold: fully supported components.

italic bold: partially supported components.

blank: not yet supported components.

  • Document

    • Operation definition

      • Operation type

        • Query

        • Mutation

        • Subscription

      • Name

      • Variable definitions

        • Type

        • Default value

        • Directives

      • Directives

      • Selection set

        • Field

          • Alias

          • Name

          • Arguments

            • Variable

            • Int value

            • Float value

            • String value

            • Boolean value

            • Null value

            • Enum value

            • List value

            • Object value

          • Directives

        • Fragment spread

        • Inline fragment

    • Fragment definition

    • TypeSystem definition

    • TypeSystem extension

Java 8 support

Java 8 is still widely used in the industry and we propose to stick to it for a broader adoption.

Next steps

To be studied for next releases:

Workflow of the API

The usual workflow of the API is illustrated with the following snippet:

// Building of the graphql document.
Document myDocument = document(
                operation(Operation.Type.QUERY,
                        field("people",
                                field("id"),
                                field("name")
                        )));

// Serialization of the document into a string, ready to be sent.
String graphqlRequest = myDocument.build();

Building a GraphQL Document

back2back
Figure 1. A GraphQL document and how to write it in Java

Static factory methods over constructors

In order to make the writing of a GraphQL request in Java as close as possible to the original GraphQL’s philosophy, it has been decided to make the usage of static factory methods an integral part of the API.

Of course, constructors can still be used but at the cost of clarity and ease of use.

Buildable

public interface Buildable {
    String build();
}

The build method is expected to return the corresponding GraphQL source of a component.

Document

public interface Document extends Buildable {

    List<? extends Operation> getOperations();
    void setOperations(List<? extends Operation> operations);
}

Static factory methods

@SafeVarargs
public static Document document(Operation... operations) {
    [...]
}

Operation

public interface Operation extends Buildable {

    enum Type {
        QUERY,
        MUTATION,
        SUBSCRIPTION
    }

    Type getType();
    void setType(Type type);

    String getName();
    void setName(String name);

    List<? extends Variable> getVariables();
    void setVariables(List<? extends Variable> vars);

    List<? extends Field> getFields();
    void setFields(List<? extends Field> fields);
}

Static factory methods

@SafeVarargs
public static List<Operation> operations(Operation... operations) {
    [...]
}

// (fields)
@SafeVarargs
public static Operation operation(Field... fields) {
    [...]
}

// (vars, fields)
@SafeVarargs
public static Operation operation(List<Variable> vars, Field... fields) {
    [...]
}

// (type, fields)
@SafeVarargs
public static Operation operation(Type type, Field... fields) {
    [...]
}

// (type, vars, fields)
@SafeVarargs
public static Operation operation(Type type, List<Variable> vars, Field... fields) {
    [...]
}

// (name, fields)
@SafeVarargs
public static Operation operation(String name, Field... fields) {
    [...]
}

// (type, name, fields)
@SafeVarargs
public static Operation operation(Type type, String name, Field... fields) {
    [...]
}

// (name, vars, fields)
@SafeVarargs
public static Operation operation(String name, List<Variable> vars, Field... fields) {
    [...]
}

// (type, name, vars, fields)
@SafeVarargs
public static Operation operation(Type type, String name, List<Variable> vars, Field... fields) {
    [...]
}

When omitted,

  • Operation’s type parameter will default to QUERY.

  • Operation’s name parameter will default to an empty string.

  • Operation’s vars parameter will default to an empty list.

Variable

public interface Variable extends Buildable {

    String getName();
    void setName(String name);

    VariableType getType();
    void setType(VariableType value);

    Object getDefaultValue();
    void setDefaultValue(Object value);
}

Static factory methods

@SafeVarargs
public static List<Variable> vars(Variable... vars) {
    [...]
}

// (name, scalarType)
public static Variable var(String name, ScalarType scalarType) {
    [...]
}

// (name, scalarType, defaultValue)
public static Variable var(String name, ScalarType scalarType, Object defaultValue) {
    [...]
}

// (name, objectType)
public static Variable var(String name, String objectType) {
    [...]
}

// (name, objectType, defaultValue)
public static Variable var(String name, String objectType, Object defaultValue) {
    [...]
}

// (name, VariableType)
public static Variable var(String name, VariableType type) {
    [...]
}

// (name, VariableType, defaultValue)
public static Variable var(String name, VariableType type, Object defaultValue) {
    [...]
}

Scalar type

public enum ScalarType {
    GQL_INT("Int"),
    GQL_FLOAT("Float"),
    GQL_STRING("String"),
    GQL_BOOL("Boolean"),
    GQL_ID("ID");

    private String type;

    ScalarType(String type) {
        this.type = type;
    }

    public String toString() {
        return type;
    }
}

The ScalarType enum is meant to represents the basic scalar types as described in the GraphQL spec (https://spec.graphql.org/draft/#sec-Scalars).

Variable type

public interface VariableType extends Buildable {

    String getName();
    void setName(String name);

    boolean isNonNull();
    void setNonNull(boolean nonNull);

    VariableType getChild();
    void setChild(VariableType child);

    default boolean isList() {
        return getChild() != null;
    }
}

Static factory methods

// (scalarType)
public static VariableType nonNull(ScalarType scalarType) {
    [...]
}

// (objectType)
public static VariableType nonNull(String name) {
    [...]
}

// (varType object)
public static VariableType nonNull(VariableType type) {
    [...]
}

// (scalarType)
public static VariableType list(ScalarType scalarType) {
    [...]
}

// (typeName)
public static VariableType list(String name) {
    [...]
}

// (variableType object)
public static VariableType list(VariableType childVarType) {
    [...]
}

Field

public interface Field extends Buildable {

    String getName();
    void setName(String name);

    List<? extends Argument> getArguments();
    void setArguments(List<? extends Argument> arguments);

    List<? extends Field> getFields();
    void setFields(List<? extends Field> fields);
}

Static factory methods

@SafeVarargs
public static List<Field> fields(Field... fields) {
    [...]
}

// (name)
public static Field field(String name) {
    [...]
}

// (name, subfields)
@SafeVarargs
public static Field field(String name, Field... fields) {
    [...]
}

// (name, args)
@SafeVarargs
public static Field field(String name, Argument... args) {
    [...]
}

// (name, args, subfields)
@SafeVarargs
public static Field field(String name, List<Argument> args, Field... fields) {
    [...]
}

When omitted, args and fields parameters will default to an empty list.

Argument

public interface Argument extends Buildable {

    String getName();
    void setName(String name);

    Object getValue();
    void setValue(Object value);
}

Static factory methods

@SafeVarargs
public static List<Argument> args(Argument... args) {
    [...]
}

// (name, raw value)
public static Argument arg(String name, Object value) {
    [...]
}

// (name, inputObject)
public static Argument arg(String name, InputObject inputObject) {
    [...]
}

// (name, variable)
public static Argument arg(String name, Variable var) {
    [...]
}

Input Object

public interface InputObject extends Buildable {

    List<? extends InputObjectField> getInputObjectFields();
    void setInputObjectFields(List<? extends InputObjectField> inputObjectFields);
}

Static factory methods

@SafeVarargs
public static InputObject inputObject(InputObjectField... inputObjectFields) {
    [...]
}

Input Object Field

public interface InputObjectField extends Buildable {

    String getName();
    void setName(String name);

    Object getValue();
    void setValue(Object value);
}

Static factory methods

// (name, value)
public static InputObjectField prop(String name, Object value) {
    [...]
}

// (name, variable)
public static InputObjectField prop(String name, Variable var) {
    [...]
}

The keyword prop (as in an object’s property) has been chosen instead of field to avoid confusion with the notion of field of a selection set.

Enum

public interface Enum {

    String getValue();
    void setValue(String value);
}

Static factory methods

public static Enum gqlEnum(String value);

Due to Java’s reserved keyword enum, the prefixe gql have been added for the static factory method.

Running a GraphQL document

Once a GraphQL document has been prepared, it can be run against a server. This specification proposes two abstractions for that:

  1. Request: prepare a request execution including the request and optional variables.

  2. Response: a holder for a GraphQL response including optional errors and data.

GraphQLClientBuilder

A ClientBuilder class is defined to bootstrap a client implementation. This can be done using the Service Loader approach.

Interface defintion

public interface ClientBuilder {
    Request newRequest(String request);
}

Request

Interface Definition

public interface Request {

    Request addVariable(String name, Object value);

    Request resetVariables();

    String toJson();
}

Initialization

A Request object is initialised from the builder with a GraphQL request obtained from a Document:

Request graphQLRequest = graphQLClientBuilder.newRequest(document.build());

Setting variables

Optional GraphQL variables can be provided in a fluent manner:

graphQLRequest
    .addVariable("surname", "James")
    .addVariable("personId", 1);

In order to make it reuseable for other executions, variables can also be reset:

graphQLRequest
    .resetVariables()
    .addVariable("surname", "Roux")
    .addVariable("personId", 2);

With this approach, a Request object is immutable regarding the GraphQL document to send and mutable regarding the variables. It is the responsibility of the caller to ensure the consistency between the request and the variables.

Once initialized with a document and optional variables, a Request object can be sent to a GraphQL server. As mentioned in the "non-goal" paragraph, this specification is deliberatly transport agnostic. It is the responsibility of the implementation to propose a transport layer.

For instance:

  • JAX-RS in a Jakarta EE or MicroProfile container

  • raw HTTP using a library such as Apache HTTP client.

Examples of JAX-RS transport

To make things more concrete, we propose some examples using JAX-RS.

Suppose we a have an initialized Request. It can be a mutation or a query. We can send it and get the response in the following way;

Client client = clientBuilder.build();

Response response = client
        .target("http://localhost:8080/graphql")
        .request(MediaType.APPLICATION_JSON)
        .post(json(graphQLRequest));

A registered JAX-RS MessageBodyWriter is needed to automatically turn a GraphQLRequest object into a JSON structure. This is the responsibility of the implementation to provide it.

In the previous example, a generic JAX-RS Response is returned. The GraphQLResponse (described below) can then be read as an entity:

Response graphQLResponse = response
    .readEntity(Response.class);

Alternatively, we can get a Response directly as a typed entity:

Response graphQLResponse = client
        .target("http://localhost:8080/graphql")
        .request(MediaType.APPLICATION_JSON)
        .post(json(graphQLRequest), Response.class);

A registered JAX-RS MessageBodyReader is needed to turn a JSON structure into a Response object. This is the responsibility of the implementation to provide it.

Using JAX-RS, we can even run a request in a reactive way:

CompletionStage<Response> csr = client
        .target("http://localhost:8080/graphql")
        .request()
        .rx()
        .post(json(graphQLRequest), Response.class);

        // Do some other stuff here...

        csr.thenAccept(// Async processing here });

Examples of HTTP transport

Let’s see how to use a HTTP transport layer with Apache HttpClient:

// Prepare the HTTP POST
URI endpoint = new URI("http://localhost:8080/graphql");
HttpPost httpPost = new HttpPost(new URI(endpoint));

StringEntity stringEntity = new StringEntity(jsonRequest.toJson(), ContentType.APPLICATION_JSON);
httpPost.setEntity(stringEntity);

// Execute the POST
CloseableHttpClient httpClient = HttpClients.createDefault());
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);

// Read the response
InputStream contentStream = serverResponse.getEntity().getContent();

For the sake of simplicity, this code does not take into account configuration, exception and resource management and omits the details of data conversion.

Response

In the previous examples, we have seen how to get a GraphQLResponse from a server.

GraphQLResponse is a holder both for data and errors.

Interface definition

public interface Response {

    JsonObject getData();
    List<Error> getErrors();

    <T> List<T> getList(Class<T> dataType, String rootField);
    <T> T getObject(Class<T> dataType, String rootField);

    boolean hasData();

    boolean hasError();
}
public interface Error {

    String getMessage();
    List<Map<String, Integer>> getLocations();

    Object[] getPath();
    Map<String, Object> getExtensions();
}

Getting errors

We can check if there is any error and access each of them:

if ( graphQLResponse.hasError() ) {
    log.warn("GraphQL error:");
    graphQLResponse.getErrors().forEach( e -> log.warning(e.toString()) );
}

The getErrors() method returns a list of Error objects. In accordance with the specification, a Error is made of:

  • a message

  • a list of locations

  • an array of path

  • a map of extensions

It is the responsibility of the client to decide how to deal with GraphQL errors.

Getting data

The hasData method enables to check if there is any data:

if (graphQLResponse.hasData())
    log.info("Data inside");

Data can be obtained in 2 ways:

  • as a generic JsonObject: using the getData method, it is the responsibility of the caller to turn this JsonObject into application objects.

  • as an application object (or a list of them): using the getObject (or getList) method. In that case, it is necessary to provide the expected data rootfield to be retrieved.

For instance, with a UserProfile application class:

// Get the data as a generic JsonObject
JsonObject data = graphQLResponse.getData();

// Turn it into a UserProfile object
JsonObject myData = data.getJsonObject("profile");
Jsonb jsonb = JsonbBuilder.create();
UserProfile userProfile = jsonb.fromJson(myData.toString(), Profile.class);

// OR

// Directly get a UserProfile object from graphqlReponse
UserProfile userProfile = graphQLResponse.getObject(Profile.class, "profile");

In the same way, the getList method enables to get a list of objects:

// Get a list of Person from a graphQLResponse
List<Person> people = graphQLResponse.getList(Person.class, "people");

About

A GraphQL Client in Java (inspired by JAX-RS Client)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages