diff --git a/quickstart-client/README.md b/quickstart-client/README.md new file mode 100644 index 0000000..539f6e3 --- /dev/null +++ b/quickstart-client/README.md @@ -0,0 +1,145 @@ +# GraphQL Client quickstart + +## Prerequisites +- JDK 11+ +- Maven + +## Quickstart's compatibility with server-side quickstart +This quickstart requires the server-side quickstart implementation to function properly. Instructions for deploying the feature pack on the server-side can be found in the server-side quickstart's [README](../quickstart/README.md). + + +## Building and deployment + +The [main README](../README.md) contains information about the layers in this feature pack. You can use the `wildfly-maven-plugin` to build and run the server with the feature pack, and deploy the quickstart war. + +``` +mvn wildfly:provision wildfly:dev +``` + + +## Functionality +The quickstart-client.war application exposes various REST endpoints that demonstrate interacting with the server-side GraphQL API through the SmallRye GraphQL Client. +The endpoints utilize both typesafe and dynamic client approaches. +### Typesafe API + +The typesafe client functions like a MicroProfile REST Client but is tailored for GraphQL endpoints. To use it, you will need domain model classes that match the GraphQL schema. + +Think of a client instance as a proxy. You interact with it as you would a regular Java object, and it translates your calls into GraphQL operations. + +It directly handles domain classes, translating input and output data between Java objects and their representations in the GraphQL query language. +#### Example + +Let's start by defining a domain class `Film` to represent the data we will be working with. +```java +package org.wildfly.extras.quickstart.microprofile.graphql.client; + +import java.time.LocalDate; + +public class Film { + String title; + Integer episodeID; + String director; + LocalDate releaseDate; + String desc; +} +``` +Next, create a GraphQL client API interface `FilmClientApi` to define the GraphQL operations: +```java +@GraphQLClientApi +interface FilmClientApi { + @Query + List getAllFilms(); +} +``` + +Finally, implement a REST endpoint to expose the GraphQL client's functionality: +```java +@GET +@Path("/typesafe/films") +@Produces(MediaType.APPLICATION_JSON) +public List getAllFilms() { + FilmClientApi client = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint(URL) // http://localhost:8080/quickstart/graphql + .build(FilmClientApi.class); + return client.getAllFilms(); +} +``` +You can test the endpoint using a tool like `curl`: +``` +curl localhost:8080/quickstart-client/typesafe/films +``` +The typesafe client will automatically generate the corresponding GraphQL query based on the operation's return type and domain classes: +``` +query allFilms { + allFilms { + title + pisodeID + director + releaseDate + desc + } +} +``` +The proxy will then send this query as part of the GraphQL request to the server. The server will process the request and return a GraphQL response. +The response will be deserialized into Java objects. + +Other endpoints: +- **`localhost:8080/typesafe/films/{id}`:** Retrieves a specific film by its index. +- **`localhost:8080/typesafe/delete/hero/{id}`:** Deletes a hero by its index. +- **`localhost:8080/typesafe/heroes/{surname}`:** Retrieves a list of heroes with a specific surname. + +> [NOTE] +> The `VertxTypesafeGraphQLClientBuilder` (*Vert.x*'s typesafe implementation) allows to inject a pre-build `ClientModels` bean instance–which has been generated during deployment process via Jandex API. +> This means you can provide the pre-generated GraphQL queries directly to the typesafe client builder, bypassing the default generation process of using Java Reflection during runtime. +> ```java +> @Inject +> ClientModels clientmodels; +> // ... +> FilmClientApi client = new VertxTypesafeGraphQLClientBuilder() +> .clientModels(clientModels) +> .endpoint(URL) +> .build(FilmClientApi.class); +> // ... +>``` + +### Dynamic API +Unlike the typesafe API, the dynamic client does not require a client API interface or domain classes. It operates directly with abstract representations of GraphQL documents, constructed using a domain-specific language (DSL). +Exchanged objects are treated as abstract `JsonObject`, but can be converted to concrete model objects if necessary. +#### Example +```java +@GET +@Path("/dynamic/films/{id}") +@Produces(MediaType.APPLICATION_JSON) +public Film getFilmDynamic(@PathParam("id") int id) throws ExecutionException, InterruptedException { + try (VertxDynamicGraphQLClient dynamicGraphQLClient = + (VertxDynamicGraphQLClient) new VertxDynamicGraphQLClientBuilder() + .url(URL) + .build() + ) { + + Variable filmId = var("filmId", nonNull(ScalarType.GQL_INT)); + + Document query = document( + operation("film", vars(filmId), + field("film", args(arg("filmId", filmId)), + field("title"), + field("episodeID"), + field("director"), + field("releaseDate"), + field("desc") + ) + ) + ); + Response response = dynamicGraphQLClient.executeSync(query, Collections.singletonMap("filmId", id)); + return response.getObject(Film.class, "film"); + } +} +``` +You can test the endpoint using a tool like `curl`: +``` +curl localhost:8080/quickstart-client/dynamic/films/{id} +``` + + + + diff --git a/quickstart-client/pom.xml b/quickstart-client/pom.xml new file mode 100644 index 0000000..9bb2fb0 --- /dev/null +++ b/quickstart-client/pom.xml @@ -0,0 +1,209 @@ + + + + 4.0.0 + + + wildfly-microprofile-graphql-parent + org.wildfly.extras.graphql + 2.4.2.Final-SNAPSHOT + + + wildfly-microprofile-graphql-quickstart-client + 2.4.2.Final-SNAPSHOT + war + WildFly MicroProfile GraphQL - Quickstart Client + Quickstart for the WildFly implementation of MicroProfile GraphQL Client + + + true + 3.13.0 + + ${project.basedir}/target/wildfly + ${jboss.dist} + + 127.0.0.2 + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + + + io.smallrye + smallrye-graphql-client-api + ${version.io.smallrye.graphql} + + + io.smallrye + smallrye-graphql-client + ${version.io.smallrye.graphql} + + + + + + + io.smallrye + smallrye-graphql-client-api + provided + + + io.smallrye + smallrye-graphql-client-implementation-vertx + provided + + + io.smallrye + smallrye-graphql-client-model + ${version.io.smallrye.graphql} + provided + + + jakarta.inject + jakarta.inject-api + provided + + + org.eclipse.microprofile.graphql + microprofile-graphql-api + provided + + + jakarta.ws.rs + jakarta.ws.rs-api + provided + + + + quickstart-client + + + + org.wildfly.plugins + wildfly-maven-plugin + + + + wildfly@maven(org.jboss.universe:community-universe):current#${version.org.wildfly} + + + org.wildfly.extras.graphql:wildfly-microprofile-graphql-feature-pack:${project.version} + + + + cloud-server + jmx-remoting + management + microprofile-graphql + micrometer + microprofile-telemetry + + + + + + + org.jboss.galleon + galleon-maven-plugin + + + server-provisioning + + provision + + test-compile + + ${project.build.directory}/wildfly-test + false + ${galleon.log.time} + + + ${galleon.fork.embedded} + passive+ + + + + true + org.wildfly + wildfly-galleon-pack + ${version.org.wildfly} + false + false + + + ${project.groupId} + wildfly-microprofile-graphql-feature-pack + ${project.version} + false + false + + + + + standalone + standalone.xml + + jaxrs-server + jmx-remoting + observability + + microprofile-graphql + micrometer + microprofile-telemetry + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.build.directory}/wildfly-test + -Djboss.bind.address=${node0} -Djboss.bind.address.management=${node0} -Djboss.bind.address.unsecure=${node0} + ${node0} + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + + + diff --git a/quickstart-client/provision.xml b/quickstart-client/provision.xml new file mode 100644 index 0000000..93e27af --- /dev/null +++ b/quickstart-client/provision.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/Application.java b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/Application.java new file mode 100644 index 0000000..59aa6e1 --- /dev/null +++ b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/Application.java @@ -0,0 +1,15 @@ +package org.wildfly.extras.quickstart.microprofile.graphql.client; + +import jakarta.ws.rs.ApplicationPath; + +import java.util.Collections; +import java.util.Set; + +@ApplicationPath("/") +public class Application extends jakarta.ws.rs.core.Application { + + @Override + public Set> getClasses() { + return Collections.singleton(FilmResource.class); + } +} \ No newline at end of file diff --git a/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/FilmClientApi.java b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/FilmClientApi.java new file mode 100644 index 0000000..4dd0ed8 --- /dev/null +++ b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/FilmClientApi.java @@ -0,0 +1,25 @@ +package org.wildfly.extras.quickstart.microprofile.graphql.client; + +import java.util.List; + +import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; +import org.wildfly.extras.quickstart.microprofile.graphql.client.model.Film; +import org.wildfly.extras.quickstart.microprofile.graphql.client.model.Hero; + +@GraphQLClientApi +interface FilmClientApi { + @Query + List getAllFilms(); + + @Query + Film getFilm(@Name("filmId") int id); + + @Mutation + Hero deleteHero(int id); + + @Query + List getHeroesWithSurname(String surname); +} diff --git a/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/FilmResource.java b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/FilmResource.java new file mode 100644 index 0000000..e08d68f --- /dev/null +++ b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/FilmResource.java @@ -0,0 +1,124 @@ +package org.wildfly.extras.quickstart.microprofile.graphql.client; + +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.core.Document; +import io.smallrye.graphql.client.core.ScalarType; +import io.smallrye.graphql.client.core.Variable; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; +import io.smallrye.graphql.client.model.ClientModels; +import io.smallrye.graphql.client.typesafe.api.TypesafeGraphQLClientBuilder; +import io.smallrye.graphql.client.vertx.typesafe.VertxTypesafeGraphQLClientBuilder; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.wildfly.extras.quickstart.microprofile.graphql.client.model.Film; +import org.wildfly.extras.quickstart.microprofile.graphql.client.model.Hero; + +import java.util.Collections; +import java.util.List; + +import static io.smallrye.graphql.client.core.Argument.arg; +import static io.smallrye.graphql.client.core.Argument.args; +import static io.smallrye.graphql.client.core.Document.document; +import static io.smallrye.graphql.client.core.Field.field; +import static io.smallrye.graphql.client.core.Operation.operation; +import static io.smallrye.graphql.client.core.Variable.var; +import static io.smallrye.graphql.client.core.Variable.vars; +import static io.smallrye.graphql.client.core.VariableType.nonNull; + +@Path("/") +public class FilmResource { + + @Inject + ClientModels clientModels; + + private static final String URL; + + static { + final Integer port = Integer.getInteger("port", 8080); + URL = "http://localhost:" + port + "/quickstart/graphql"; + } + + @GET + @Path("/typesafe/films") + @Produces(MediaType.APPLICATION_JSON) + public List getAllFilms() { + System.out.println("Using URL " + URL); + FilmClientApi client = + // VertxTypesafeGraphQLClientBuilder allows adding an injected ClientModels bean instance + // to be added into the builder. ClientModels contains all the pre-generated queries during deployment time. + // Queries are build via Jandex API–instead of Java Reflection. + + // TypesafeGraphQLClientBuilder.newBuilder() + // .endpoint(url) + // .build(FilmClientApi.class); + new VertxTypesafeGraphQLClientBuilder() + .clientModels(clientModels) + .endpoint(URL) + .build(FilmClientApi.class); + return client.getAllFilms(); + } + + @GET + @Path("/typesafe/films/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Film getFilm(@PathParam("id") int index) { + System.out.println("Using URL " + URL); + FilmClientApi client = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint(URL) + .build(FilmClientApi.class); + return client.getFilm(index); + } + + @GET + @Path("/typesafe/delete/hero/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Hero deleteFilm(@PathParam("id") int id) { + System.out.println("Using URL " + URL); + FilmClientApi client = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint(URL) + .build(FilmClientApi.class); + return client.deleteHero(id); + } + + @GET + @Path("/typesafe/heroes/{surname}") + @Produces(MediaType.APPLICATION_JSON) + public List getHeroesWithSurname(@PathParam("surname") String surname) { + System.out.println("Using URL " + URL); + FilmClientApi client = TypesafeGraphQLClientBuilder.newBuilder() + .endpoint(URL) + .build(FilmClientApi.class); + return client.getHeroesWithSurname(surname); + } + + @GET + @Path("/dynamic/films/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Film getFilmDynamic(@PathParam("id") int id) throws Exception { + try (DynamicGraphQLClient dynamicGraphQLClient = DynamicGraphQLClientBuilder + .newBuilder() + .url(URL) + .build() + ){ + Variable filmId = var("filmId", nonNull(ScalarType.GQL_INT)); + Document query = document( + operation("film", vars(filmId), + field("film", args(arg("filmId", filmId)), + field("title"), + field("episodeID"), + field("director"), + field("releaseDate"), + field("desc") + ) + ) + ); + Response response = dynamicGraphQLClient.executeSync(query, Collections.singletonMap("filmId", id)); + return response.getObject(Film.class, "film"); + } + } +} diff --git a/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/Film.java b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/Film.java new file mode 100644 index 0000000..173a355 --- /dev/null +++ b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/Film.java @@ -0,0 +1,52 @@ +package org.wildfly.extras.quickstart.microprofile.graphql.client.model; + +import java.time.LocalDate; + +public class Film { + + private String title; + private Integer episodeID; + private String director; + private LocalDate releaseDate; + private String desc; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Integer getEpisodeID() { + return episodeID; + } + + public void setEpisodeID(Integer episodeID) { + this.episodeID = episodeID; + } + + public String getDirector() { + return director; + } + + public void setDirector(String director) { + this.director = director; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } +} \ No newline at end of file diff --git a/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/Hero.java b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/Hero.java new file mode 100644 index 0000000..543772f --- /dev/null +++ b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/Hero.java @@ -0,0 +1,72 @@ +package org.wildfly.extras.quickstart.microprofile.graphql.client.model; + + +import java.util.ArrayList; +import java.util.List; + +public class Hero { + + private String name; + private String surname; + private Double height; + private Integer mass; + private Boolean darkSide; + private LightSaber lightSaber; + private List episodeIds = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = surname; + } + + public Double getHeight() { + return height; + } + + public void setHeight(Double height) { + this.height = height; + } + + public Integer getMass() { + return mass; + } + + public void setMass(Integer mass) { + this.mass = mass; + } + + public Boolean getDarkSide() { + return darkSide; + } + + public void setDarkSide(Boolean darkSide) { + this.darkSide = darkSide; + } + + public LightSaber getLightSaber() { + return lightSaber; + } + + public void setLightSaber(LightSaber lightSaber) { + this.lightSaber = lightSaber; + } + + public List getEpisodeIds() { + return episodeIds; + } + + public void setEpisodeIds(List episodeIds) { + this.episodeIds = episodeIds; + } +} \ No newline at end of file diff --git a/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/LightSaber.java b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/LightSaber.java new file mode 100644 index 0000000..cbcf7f8 --- /dev/null +++ b/quickstart-client/src/main/java/org/wildfly/extras/quickstart/microprofile/graphql/client/model/LightSaber.java @@ -0,0 +1,5 @@ +package org.wildfly.extras.quickstart.microprofile.graphql.client.model; + +public enum LightSaber { + RED, BLUE, GREEN, PURPLE +} diff --git a/quickstart-client/src/main/resources/META-INF/microprofile-config.properties b/quickstart-client/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 0000000..e69de29 diff --git a/quickstart-client/src/main/webapp/WEB-INF/beans.xml b/quickstart-client/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 0000000..ffdfc41 --- /dev/null +++ b/quickstart-client/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/quickstart-client/src/main/webapp/WEB-INF/web.xml b/quickstart-client/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..2ab35d4 --- /dev/null +++ b/quickstart-client/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,9 @@ + + + + +