├── vertx-graphql-mic-drop.png ├── rental-service ├── src │ └── main │ │ ├── conf │ │ └── config.json │ │ ├── resources │ │ └── log4j2.xml │ │ └── java │ │ └── com │ │ └── github │ │ └── bmsantos │ │ └── rental │ │ └── app │ │ └── AppVerticle.java └── pom.xml ├── graphql-service ├── src │ ├── main │ │ ├── conf │ │ │ ├── local.json │ │ │ └── docker.json │ │ ├── schema │ │ │ ├── rental-schema.graphql │ │ │ ├── customer-schema.graphql │ │ │ └── vehicle-schema.graphql │ │ ├── java │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── bmsantos │ │ │ │ └── graphql │ │ │ │ ├── utils │ │ │ │ ├── VertxCompletableFutureUtils.java │ │ │ │ ├── VertxCompletableFutureFactory.java │ │ │ │ ├── CompletableObserver.java │ │ │ │ └── UnmarshallerOperator.java │ │ │ │ ├── apigen │ │ │ │ ├── queries │ │ │ │ │ ├── QueryRentalsImpl.java │ │ │ │ │ ├── QueryVehiclesImpl.java │ │ │ │ │ ├── QueryCustomersImpl.java │ │ │ │ │ └── MutateVehiclesImpl.java │ │ │ │ ├── json │ │ │ │ │ ├── CustomerDeserializer.java │ │ │ │ │ ├── RentalDeserializer.java │ │ │ │ │ └── VehicleDeserializer.java │ │ │ │ ├── resolvers │ │ │ │ │ ├── VehicleResolver.java │ │ │ │ │ ├── CustomerResolver.java │ │ │ │ │ └── RentalResolver.java │ │ │ │ └── guice │ │ │ │ │ └── AppModule.java │ │ │ │ ├── engine │ │ │ │ └── GraphQLEngine.java │ │ │ │ ├── AppVerticle.java │ │ │ │ ├── rest │ │ │ │ ├── GraphQLHandler.java │ │ │ │ └── RestClient.java │ │ │ │ └── dataloaders │ │ │ │ └── DataLoaders.java │ │ └── resources │ │ │ └── log4j2.xml │ └── test │ │ └── java │ │ └── com │ │ └── github │ │ └── bmsantos │ │ └── graphql │ │ ├── model │ │ ├── MutateVehiclesTest.java │ │ ├── QueryVehiclesTest.java │ │ ├── QueryCustomersTest.java │ │ ├── QueryRentalsTest.java │ │ └── BaseGraphQLTest.java │ │ └── apigen │ │ └── resolvers │ │ ├── TestableRentalResolver.java │ │ ├── TestableVehicleResolver.java │ │ └── TestableCustomerResolver.java └── pom.xml ├── docker ├── run.sh ├── basic.sh └── docker-compose.yml ├── vehicle-service ├── src │ └── main │ │ ├── conf │ │ └── config.json │ │ ├── resources │ │ └── log4j2.xml │ │ └── java │ │ └── com │ │ └── github │ │ └── bmsantos │ │ └── vehicle │ │ └── app │ │ └── AppVerticle.java └── pom.xml ├── customer-service ├── src │ └── main │ │ ├── conf │ │ └── config.json │ │ ├── resources │ │ └── log4j2.xml │ │ └── java │ │ └── com │ │ └── github │ │ └── bmsantos │ │ └── customer │ │ └── app │ │ └── AppVerticle.java └── pom.xml ├── README.md └── pom.xml /vertx-graphql-mic-drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doyleyoung/vertx-graphql-example/HEAD/vertx-graphql-mic-drop.png -------------------------------------------------------------------------------- /rental-service/src/main/conf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "rentals": [ 3 | { 4 | "id": 0, 5 | "customer": 0, 6 | "vehicle": 0 7 | }, 8 | { 9 | "id": 1, 10 | "customer": 1, 11 | "vehicle": 1 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /graphql-service/src/main/conf/local.json: -------------------------------------------------------------------------------- 1 | { 2 | "service.http.port": 8080, 3 | "service.http.address": "0.0.0.0", 4 | "customer.service.url": "http://localhost:8081", 5 | "vehicle.service.url": "http://localhost:8082", 6 | "rental.service.url": "http://localhost:8083" 7 | } -------------------------------------------------------------------------------- /graphql-service/src/main/conf/docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "service.http.port": 8080, 3 | "service.http.address": "0.0.0.0", 4 | "customer.service.url": "http://customer-service:8081", 5 | "vehicle.service.url": "http://vehicle-service:8082", 6 | "rental.service.url": "http://rental-service:8083" 7 | } -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Get this script directory (to find yml from any directory) 6 | export DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 7 | 8 | # Stop 9 | docker-compose -f $DIR/docker-compose.yml stop 10 | 11 | # Start 12 | docker-compose -f $DIR/docker-compose.yml up -------------------------------------------------------------------------------- /graphql-service/src/main/schema/rental-schema.graphql: -------------------------------------------------------------------------------- 1 | type Rental @java(package:"com.github.bmsantos.graphql.model.rental") { 2 | id: Long! 3 | customer: Customer 4 | vehicle: Vehicle 5 | } 6 | 7 | type QueryRentals @java(package:"com.github.bmsantos.graphql.model.rental") { 8 | rentals: [Rental] 9 | rental(id: Long): Rental 10 | } 11 | -------------------------------------------------------------------------------- /docker/basic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Get this script directory (to find yml from any directory) 6 | export DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 7 | 8 | # Stop 9 | docker-compose -f $DIR/docker-compose.yml stop 10 | 11 | docker-compose -f $DIR/docker-compose.yml up -d vehicle-service customer-service rental-service -------------------------------------------------------------------------------- /vehicle-service/src/main/conf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "vehicles": [ 3 | { 4 | "id": 0, 5 | "brand": "Ford", 6 | "model": "Explorer", 7 | "type": "SUV", 8 | "year": 2013, 9 | "mileage": 10000 10 | }, 11 | { 12 | "id": 1, 13 | "brand": "Toyota", 14 | "model": "Corolla", 15 | "type": "Car", 16 | "year": 2015, 17 | "mileage": 15000 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /customer-service/src/main/conf/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "customers": [ 3 | { 4 | "id": 0, 5 | "name": "Albert Einstein", 6 | "address": "345 Somewhere Road", 7 | "city": "New York", 8 | "state": "New York", 9 | "country": "USA" 10 | }, 11 | { 12 | "id": 1, 13 | "name": "Isaac Newton", 14 | "address": "45 Street Farm", 15 | "city": "London", 16 | "state": "London", 17 | "country": "UK" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/utils/VertxCompletableFutureUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.utils; 2 | 3 | import me.escoffier.vertx.completablefuture.VertxCompletableFuture; 4 | 5 | public class VertxCompletableFutureUtils { 6 | public static VertxCompletableFuture completedVertxCompletableFuture(final T value) { 7 | final VertxCompletableFuture future = new VertxCompletableFuture<>(); 8 | future.complete(value); 9 | return future; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/utils/VertxCompletableFutureFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.utils; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import graphql.execution.async.CompletableFutureFactory; 6 | import me.escoffier.vertx.completablefuture.VertxCompletableFuture; 7 | 8 | public class VertxCompletableFutureFactory implements CompletableFutureFactory { 9 | @Override 10 | public CompletableFuture future() { 11 | return new VertxCompletableFuture(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /graphql-service/src/main/schema/customer-schema.graphql: -------------------------------------------------------------------------------- 1 | type Contact @java(package:"com.github.bmsantos.graphql.model.customer") { 2 | phone: String 3 | type: String 4 | } 5 | 6 | type Customer @java(package:"com.github.bmsantos.graphql.model.customer") { 7 | id: Long! 8 | name: String 9 | contact: Contact 10 | address: String 11 | city: String 12 | state: String 13 | country: String 14 | } 15 | 16 | type QueryCustomers @java(package:"com.github.bmsantos.graphql.model.customer") { 17 | customers: [Customer] 18 | customer(id:Long): Customer 19 | } 20 | -------------------------------------------------------------------------------- /rental-service/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /customer-service/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /graphql-service/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /vehicle-service/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/queries/QueryRentalsImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.queries; 2 | 3 | import java.util.List; 4 | 5 | import com.github.bmsantos.graphql.model.rental.QueryRentals; 6 | import com.github.bmsantos.graphql.model.rental.Rental; 7 | import com.github.bmsantos.graphql.model.rental.Rental.Unresolved; 8 | 9 | import static java.util.Collections.emptyList; 10 | 11 | public class QueryRentalsImpl implements QueryRentals { 12 | @Override 13 | public List getRentals() { 14 | return emptyList(); 15 | } 16 | 17 | @Override 18 | public Rental rental(final RentalArgs args) { 19 | return new Unresolved(args.getId()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/queries/QueryVehiclesImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.queries; 2 | 3 | import java.util.List; 4 | 5 | import com.github.bmsantos.graphql.model.vehicles.QueryVehicles; 6 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 7 | import com.github.bmsantos.graphql.model.vehicles.Vehicle.Unresolved; 8 | 9 | import static java.util.Collections.emptyList; 10 | 11 | public class QueryVehiclesImpl implements QueryVehicles { 12 | @Override 13 | public List getVehicles() { 14 | return emptyList(); 15 | } 16 | 17 | @Override 18 | public Vehicle vehicle(final VehicleArgs args) { 19 | return new Unresolved(args.getId()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/queries/QueryCustomersImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.queries; 2 | 3 | import java.util.List; 4 | 5 | import com.github.bmsantos.graphql.model.customer.Customer; 6 | import com.github.bmsantos.graphql.model.customer.Customer.Unresolved; 7 | import com.github.bmsantos.graphql.model.customer.QueryCustomers; 8 | 9 | import static java.util.Collections.emptyList; 10 | 11 | public class QueryCustomersImpl implements QueryCustomers { 12 | @Override 13 | public List getCustomers() { 14 | return emptyList(); 15 | } 16 | 17 | @Override 18 | public Customer customer(final CustomerArgs args) { 19 | return new Unresolved(args.getId()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graphql-service/src/main/schema/vehicle-schema.graphql: -------------------------------------------------------------------------------- 1 | type Vehicle @java(package:"com.github.bmsantos.graphql.model.vehicles") { 2 | id: Long! 3 | brand: String 4 | model: String 5 | type: String 6 | year: Int 7 | mileage: Long 8 | extras: [String] 9 | } 10 | 11 | type QueryVehicles @java(package:"com.github.bmsantos.graphql.model.vehicles") { 12 | vehicles: [Vehicle] 13 | vehicle(id:Long): Vehicle 14 | } 15 | 16 | input InputVehicle @java(package:"com.github.bmsantos.graphql.model.vehicles") { 17 | brand: String! 18 | model: String! 19 | type: String! 20 | year: Int! 21 | mileage: Long 22 | extras: [String] 23 | } 24 | 25 | type MutateVehicles @java(package:"com.github.bmsantos.graphql.model.vehicles") { 26 | createVehicle(vehicle:InputVehicle): Vehicle 27 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/queries/MutateVehiclesImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.queries; 2 | 3 | import com.github.bmsantos.graphql.model.vehicles.InputVehicle; 4 | import com.github.bmsantos.graphql.model.vehicles.MutateVehicles; 5 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 6 | 7 | public class MutateVehiclesImpl implements MutateVehicles { 8 | 9 | @Override 10 | public Vehicle createVehicle(final CreateVehicleArgs args) { 11 | final InputVehicle inputVehicle = args.getVehicle(); 12 | return new Vehicle.Builder() 13 | .withBrand(inputVehicle.getBrand()) 14 | .withModel(inputVehicle.getModel()) 15 | .withType(inputVehicle.getType()) 16 | .withYear(inputVehicle.getYear()) 17 | .withMileage(inputVehicle.getMileage()) 18 | .withExtras(inputVehicle.getExtras()) 19 | .build(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rental-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.github.bmsantos 9 | vertx-graphql-example 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | rental-service 14 | 15 | 16 | com.github.bmsantos.rental.app.AppVerticle 17 | 18 | 19 | 20 | 21 | 22 | maven-shade-plugin 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /customer-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.github.bmsantos 9 | vertx-graphql-example 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | customer-service 14 | 15 | 16 | com.github.bmsantos.customer.app.AppVerticle 17 | 18 | 19 | 20 | 21 | 22 | maven-shade-plugin 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /vehicle-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.github.bmsantos 9 | vertx-graphql-example 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | vehicle-service 14 | 15 | 16 | com.github.bmsantos.vehicle.app.AppVerticle 17 | 18 | 19 | 20 | 21 | 22 | maven-shade-plugin 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/json/CustomerDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.json; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonParser; 6 | import com.fasterxml.jackson.core.ObjectCodec; 7 | import com.fasterxml.jackson.databind.DeserializationContext; 8 | import com.fasterxml.jackson.databind.JsonDeserializer; 9 | import com.fasterxml.jackson.databind.JsonNode; 10 | import com.github.bmsantos.graphql.model.customer.Customer; 11 | 12 | public class CustomerDeserializer extends JsonDeserializer { 13 | @Override 14 | public Customer deserialize(final JsonParser parser, final DeserializationContext ctx) throws IOException { 15 | final ObjectCodec oc = parser.getCodec(); 16 | final JsonNode node = oc.readTree(parser); 17 | 18 | return new Customer.Builder() 19 | .withId(node.get("id").asLong()) 20 | .withName(node.get("name").asText()) 21 | .withAddress(node.get("address").asText()) 22 | .withCity(node.get("city").asText()) 23 | .withState(node.get("state").asText()) 24 | .withCountry(node.get("country").asText()) 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/json/RentalDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.json; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonParser; 6 | import com.fasterxml.jackson.core.ObjectCodec; 7 | import com.fasterxml.jackson.databind.DeserializationContext; 8 | import com.fasterxml.jackson.databind.JsonDeserializer; 9 | import com.fasterxml.jackson.databind.JsonNode; 10 | import com.github.bmsantos.graphql.model.customer.Customer; 11 | import com.github.bmsantos.graphql.model.rental.Rental; 12 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 13 | 14 | public class RentalDeserializer extends JsonDeserializer { 15 | @Override 16 | public Rental deserialize(final JsonParser parser, final DeserializationContext ctx) throws IOException { 17 | final ObjectCodec oc = parser.getCodec(); 18 | final JsonNode node = oc.readTree(parser); 19 | 20 | final Long id = node.get("id").asLong(); 21 | final Long customerId = node.get("customer").asLong(); 22 | final Long vehicleId = node.get("vehicle").asLong(); 23 | return new Rental.Builder() 24 | .withId(id) 25 | .withCustomer(new Customer.Unresolved(customerId)) 26 | .withVehicle(new Vehicle.Unresolved(vehicleId)) 27 | .build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/json/VehicleDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.json; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import com.fasterxml.jackson.core.JsonParser; 8 | import com.fasterxml.jackson.core.ObjectCodec; 9 | import com.fasterxml.jackson.databind.DeserializationContext; 10 | import com.fasterxml.jackson.databind.JsonDeserializer; 11 | import com.fasterxml.jackson.databind.JsonNode; 12 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 13 | 14 | import static java.util.Objects.nonNull; 15 | 16 | public class VehicleDeserializer extends JsonDeserializer { 17 | @Override 18 | public Vehicle deserialize(final JsonParser parser, final DeserializationContext ctx) throws IOException { 19 | final ObjectCodec oc = parser.getCodec(); 20 | final JsonNode node = oc.readTree(parser); 21 | 22 | final List extras = new ArrayList<>(); 23 | JsonNode extrasJN = node.get("extras"); 24 | if (nonNull(extrasJN)) { 25 | extrasJN.forEach( it -> extras.add(it.asText())); 26 | } 27 | 28 | return new Vehicle.Builder() 29 | .withId(node.get("id").asLong()) 30 | .withBrand(node.get("brand").asText()) 31 | .withModel(node.get("model").asText()) 32 | .withType(node.get("type").asText()) 33 | .withYear(node.get("year").asInt()) 34 | .withMileage(node.get("mileage").asLong()) 35 | .withExtras(extras) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/utils/CompletableObserver.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.utils; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import io.vertx.core.Future; 6 | import rx.Observer; 7 | 8 | import static java.util.Objects.isNull; 9 | 10 | public class CompletableObserver implements Observer { 11 | 12 | public static Observer completableObserver(final CompletableFuture future) { 13 | return new CompletableObserver<>(future); 14 | } 15 | 16 | public static Observer completableObserver(final Future future) { 17 | return new CompletableObserver<>(future); 18 | } 19 | 20 | private CompletableFuture completableFuture; 21 | private Future future; 22 | 23 | public CompletableObserver(final CompletableFuture completableFuture) { 24 | this.completableFuture = completableFuture; 25 | } 26 | public CompletableObserver(final Future future) { 27 | this.future = future; 28 | } 29 | 30 | @Override 31 | public void onCompleted() { 32 | // Empty 33 | } 34 | 35 | @Override 36 | public void onError(final Throwable t) { 37 | if (isNull(completableFuture)) { 38 | future.fail(t); 39 | } else { 40 | completableFuture.completeExceptionally(t); 41 | } 42 | } 43 | 44 | @Override 45 | public void onNext(final T emission) { 46 | if (isNull(completableFuture)) { 47 | future.complete(emission); 48 | } else { 49 | completableFuture.complete(emission); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/model/MutateVehiclesTest.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.model; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | import com.google.inject.Injector; 6 | import graphql.ExecutionResult; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | import static java.util.Collections.emptyMap; 14 | 15 | @RunWith(VertxUnitRunner.class) 16 | public class MutateVehiclesTest extends BaseGraphQLTest { 17 | 18 | private Injector injector; 19 | 20 | @Before 21 | public void setup(TestContext context) throws Exception { 22 | super.setup(context); 23 | injector = setupVehiclesInjector(); 24 | initGraphQLAsync(injector, "QueryVehicles", "MutateVehicles"); 25 | } 26 | 27 | @Test 28 | public void shouldCreateNewVehicle(TestContext context) throws Exception { 29 | // Given 30 | String query = 31 | "mutation { createVehicle(vehicle: { brand: \"Ford\" model: \"Mustang\" type: \"Car\" year: 2016 }) { id model brand }}"; 32 | 33 | // When 34 | CompletionStage future = 35 | graphQL.executeAsync(query, null, null, emptyMap()); 36 | 37 | // Then 38 | printAndValidate(future, doc -> { 39 | context.assertTrue(doc.contains("id\":3")); 40 | context.assertTrue(doc.contains("brand\":\"Ford")); 41 | context.assertTrue(doc.contains("model\":\"Mustang")); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/engine/GraphQLEngine.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.engine; 2 | 3 | import java.util.HashSet; 4 | import java.util.Map; 5 | 6 | import javax.inject.Inject; 7 | 8 | import com.github.bmsantos.graphql.utils.VertxCompletableFutureFactory; 9 | import graphql.GraphQLAsync; 10 | import graphql.schema.GraphQLObjectType; 11 | import graphql.schema.GraphQLSchema; 12 | import graphql.schema.GraphQLType; 13 | import io.vertx.core.logging.Logger; 14 | 15 | import static graphql.execution.async.AsyncExecutionStrategy.parallel; 16 | import static io.vertx.core.logging.LoggerFactory.getLogger; 17 | import static java.util.Objects.isNull; 18 | 19 | public class GraphQLEngine { 20 | private static Logger log = getLogger(GraphQLEngine.class); 21 | 22 | @Inject 23 | private Map types; 24 | 25 | private static GraphQLAsync graphQL; 26 | 27 | public GraphQLAsync engine() { 28 | if (isNull(graphQL)) { 29 | graphQL = createGraphQL(); 30 | } 31 | return graphQL; 32 | } 33 | 34 | private GraphQLAsync createGraphQL() { 35 | try { 36 | GraphQLSchema schema = GraphQLSchema.newSchema() 37 | .query((GraphQLObjectType) types.get("QueryRentals")) 38 | .build(new HashSet<>(types.values())); 39 | return new GraphQLAsync(schema, parallel(new VertxCompletableFutureFactory())); 40 | } catch (final Exception e) { 41 | final String error = "Unable to instantiate Guest GraphQL Schema"; 42 | log.error(error, e); 43 | throw new RuntimeException(error, e); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/apigen/resolvers/TestableRentalResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.resolvers; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | import com.github.bmsantos.graphql.model.rental.Rental; 8 | 9 | import static com.github.bmsantos.graphql.utils.VertxCompletableFutureUtils.completedVertxCompletableFuture; 10 | import static com.google.common.collect.Lists.newArrayList; 11 | import static java.util.Objects.isNull; 12 | import static java.util.Objects.requireNonNull; 13 | import static java.util.stream.Collectors.toList; 14 | 15 | public class TestableRentalResolver implements Rental.AsyncResolver { 16 | 17 | private Map rentals; 18 | public TestableRentalResolver(Map rentals) { 19 | this.rentals = rentals; 20 | } 21 | 22 | @Override 23 | public CompletableFuture> resolve(final List unresolved) { 24 | 25 | if (!requireNonNull(unresolved).isEmpty()) { 26 | Rental rental = unresolved.get(0); 27 | if (isNull(rental.getId())) { // is create new vehicle mutation 28 | rental = new Rental.Builder(rental).withId((long) (rentals.size() + 1)).build(); 29 | rentals.put(rental.getId(), rental); 30 | return completedVertxCompletableFuture(newArrayList(rental)); 31 | } 32 | return completedVertxCompletableFuture(unresolved.stream().map(u -> rentals.get(u.getId())).collect(toList())); // is argument query by id 33 | } 34 | 35 | return completedVertxCompletableFuture(newArrayList(rentals.values())); // is query all 36 | } 37 | 38 | @Override 39 | public CompletableFuture> resolve(final Object context, final List list) { 40 | return null; 41 | } 42 | } -------------------------------------------------------------------------------- /vehicle-service/src/main/java/com/github/bmsantos/vehicle/app/AppVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.vehicle.app; 2 | 3 | import java.util.List; 4 | 5 | import io.vertx.core.AbstractVerticle; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.json.JsonArray; 8 | import io.vertx.core.logging.Logger; 9 | import io.vertx.ext.web.Router; 10 | 11 | import static io.vertx.core.logging.LoggerFactory.getLogger; 12 | import static java.util.Objects.nonNull; 13 | 14 | public class AppVerticle extends AbstractVerticle { 15 | private static final Logger log = getLogger(AppVerticle.class); 16 | 17 | @Override 18 | public void start(final Future startFuture) throws Exception { 19 | 20 | final Router router = Router.router(vertx); 21 | 22 | router.getWithRegex("/|/vehicles").handler(req -> { 23 | log.info("GET /vehicles"); 24 | req.response().putHeader("content-type", "application/json").end(config().getJsonArray("vehicles").toString()); 25 | }); 26 | 27 | router.get("/vehicles/:id").handler(req -> { 28 | Integer id = getIntegerValue(req.request().getParam("id")); 29 | log.info("GET /vehicles/" + id); 30 | 31 | List stays = config().getJsonArray("vehicles", new JsonArray()).getList(); 32 | if (nonNull(id) && id < stays.size()) { 33 | req.response().putHeader("content-type", "application/json").end(stays.get(id).toString()); 34 | } else { 35 | req.response().setStatusCode(404).setStatusMessage("Resource not found").end(); 36 | } 37 | }); 38 | 39 | vertx.createHttpServer().requestHandler(router::accept).listen(8082); 40 | } 41 | 42 | private Integer getIntegerValue(String value) { 43 | try { 44 | return Integer.valueOf(value); 45 | } catch (Exception e) { 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/github/bmsantos/customer/app/AppVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.customer.app; 2 | 3 | import java.util.List; 4 | 5 | import io.vertx.core.AbstractVerticle; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.json.JsonArray; 8 | import io.vertx.core.logging.Logger; 9 | import io.vertx.ext.web.Router; 10 | 11 | import static io.vertx.core.logging.LoggerFactory.getLogger; 12 | import static java.util.Objects.nonNull; 13 | 14 | public class AppVerticle extends AbstractVerticle { 15 | private static final Logger log = getLogger(AppVerticle.class); 16 | 17 | @Override 18 | public void start(final Future startFuture) throws Exception { 19 | 20 | final Router router = Router.router(vertx); 21 | 22 | router.getWithRegex("/|/customers").handler(req -> { 23 | log.info("GET /customers"); 24 | req.response().putHeader("content-type", "application/json").end(config().getJsonArray("customers").toString()); 25 | }); 26 | 27 | router.get("/customers/:id").handler(req -> { 28 | Integer id = getIntegerValue(req.request().getParam("id")); 29 | log.info("GET /customers/" + id); 30 | 31 | List stays = config().getJsonArray("customers", new JsonArray()).getList(); 32 | if (nonNull(id) && id < stays.size()) { 33 | req.response().putHeader("content-type", "application/json").end(stays.get(id).toString()); 34 | } else { 35 | req.response().setStatusCode(404).setStatusMessage("Resource not found").end(); 36 | } 37 | }); 38 | 39 | vertx.createHttpServer().requestHandler(router::accept).listen(8081); 40 | } 41 | 42 | private Integer getIntegerValue(String value) { 43 | try { 44 | return Integer.valueOf(value); 45 | } catch (Exception e) { 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/apigen/resolvers/TestableVehicleResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.resolvers; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 8 | 9 | import static com.github.bmsantos.graphql.utils.VertxCompletableFutureUtils.completedVertxCompletableFuture; 10 | import static com.google.common.collect.Lists.newArrayList; 11 | import static java.util.Objects.isNull; 12 | import static java.util.Objects.requireNonNull; 13 | import static java.util.stream.Collectors.toList; 14 | 15 | public class TestableVehicleResolver implements Vehicle.AsyncResolver { 16 | private Map vehicles; 17 | 18 | public TestableVehicleResolver(Map vehicles) { 19 | this.vehicles = vehicles; 20 | } 21 | 22 | @Override 23 | public CompletableFuture> resolve(final List unresolved) { 24 | 25 | if (!requireNonNull(unresolved).isEmpty()) { 26 | Vehicle vehicle = unresolved.get(0); 27 | if (isNull(vehicle.getId())) { // is create new vehicle mutation 28 | vehicle = new Vehicle.Builder(vehicle).withId((long) (vehicles.size() + 1)).build(); 29 | vehicles.put(vehicle.getId(), vehicle); 30 | return completedVertxCompletableFuture(newArrayList(vehicle)); 31 | } 32 | return completedVertxCompletableFuture(unresolved.stream().map(u -> vehicles.get(u.getId())).collect(toList())); // is argument query by id 33 | } 34 | 35 | return completedVertxCompletableFuture(newArrayList(vehicles.values())); // is query all 36 | } 37 | 38 | @Override 39 | public CompletableFuture> resolve(final Object context, final List list) { 40 | return null; 41 | } 42 | } -------------------------------------------------------------------------------- /rental-service/src/main/java/com/github/bmsantos/rental/app/AppVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.rental.app; 2 | 3 | import java.util.List; 4 | 5 | import io.vertx.core.AbstractVerticle; 6 | import io.vertx.core.Future; 7 | import io.vertx.core.json.JsonArray; 8 | import io.vertx.core.logging.Logger; 9 | import io.vertx.ext.web.Router; 10 | 11 | import static io.vertx.core.logging.LoggerFactory.getLogger; 12 | import static java.util.Objects.nonNull; 13 | 14 | /** 15 | * REST Endpoint to simulate simple hotels. 16 | */ 17 | public class AppVerticle extends AbstractVerticle { 18 | private static final Logger log = getLogger(AppVerticle.class); 19 | 20 | @Override 21 | public void start(final Future startFuture) throws Exception { 22 | 23 | final Router router = Router.router(vertx); 24 | 25 | router.getWithRegex("/|/rentals").handler(req -> { 26 | log.info("GET /rentals"); 27 | req.response().putHeader("content-type", "application/json").end(config().getJsonArray("rentals").toString()); 28 | }); 29 | 30 | router.get("/rentals/:id").handler(req -> { 31 | Integer id = getIntegerValue(req.request().getParam("id")); 32 | log.info("GET /rentals/" + id); 33 | 34 | List hotels = config().getJsonArray("rentals", new JsonArray()).getList(); 35 | if (nonNull(id) && id < hotels.size()) { 36 | req.response().putHeader("content-type", "application/json").end(hotels.get(id).toString()); 37 | } else { 38 | req.response().setStatusCode(404).setStatusMessage("Resource not found").end(); 39 | } 40 | }); 41 | 42 | vertx.createHttpServer().requestHandler(router::accept).listen(8083); 43 | } 44 | 45 | private Integer getIntegerValue(String value) { 46 | try { 47 | return Integer.valueOf(value); 48 | } catch (Exception e) {} 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/apigen/resolvers/TestableCustomerResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.resolvers; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | import com.github.bmsantos.graphql.model.customer.Customer; 8 | 9 | import static com.github.bmsantos.graphql.utils.VertxCompletableFutureUtils.completedVertxCompletableFuture; 10 | import static com.google.common.collect.Lists.newArrayList; 11 | import static java.util.Objects.isNull; 12 | import static java.util.Objects.requireNonNull; 13 | import static java.util.stream.Collectors.toList; 14 | 15 | public class TestableCustomerResolver implements Customer.AsyncResolver { 16 | private Map customers; 17 | 18 | public TestableCustomerResolver(Map customers) { 19 | this.customers = customers; 20 | } 21 | 22 | @Override 23 | public CompletableFuture> resolve(final List unresolved) { 24 | 25 | if (!requireNonNull(unresolved).isEmpty()) { 26 | Customer customer = unresolved.get(0); 27 | if (isNull(customer.getId())) { // is create new customer mutation 28 | customer = new Customer.Builder(customer).withId((long) (customers.size() + 1)).build(); 29 | customers.put(customer.getId(), customer); 30 | return completedVertxCompletableFuture(newArrayList(customer)); 31 | } 32 | return completedVertxCompletableFuture(unresolved.stream().map(u -> customers.get(u.getId())).collect(toList())); // is argument query by id 33 | } 34 | 35 | return completedVertxCompletableFuture(newArrayList(customers.values())); // is query all 36 | } 37 | 38 | @Override 39 | public CompletableFuture> resolve(final Object context, final List list) { 40 | return null; 41 | } 42 | } -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/model/QueryVehiclesTest.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.model; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | import com.google.inject.Injector; 6 | import graphql.ExecutionResult; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | import static java.util.Collections.emptyMap; 14 | 15 | @RunWith(VertxUnitRunner.class) 16 | public class QueryVehiclesTest extends BaseGraphQLTest { 17 | 18 | private Injector injector; 19 | 20 | @Before 21 | public void setup(TestContext context) throws Exception { 22 | super.setup(context); 23 | injector = setupVehiclesInjector(); 24 | initGraphQLAsync(injector, "QueryVehicles"); 25 | } 26 | 27 | @Test 28 | public void shouldRetrieveAllVehicles(TestContext context) throws Exception { 29 | // Given 30 | String query = "{ vehicles { id brand } }"; 31 | 32 | // When 33 | CompletionStage future = 34 | graphQL.executeAsync(query, null, null, emptyMap()); 35 | 36 | // Then 37 | printAndValidate(future, doc -> { 38 | context.assertTrue(doc.contains("brand\":\"Tesla")); 39 | context.assertTrue(doc.contains("brand\":\"Toyota")); 40 | }); 41 | } 42 | 43 | @Test 44 | public void shouldRetrieveByVehicleId(TestContext context) throws Exception { 45 | // Given 46 | String query = "{ vehicle(id:1) { id brand } }"; 47 | 48 | // When 49 | CompletionStage future = 50 | graphQL.executeAsync(query, null, null, emptyMap()); 51 | 52 | // Then 53 | printAndValidate(future, doc -> { 54 | context.assertFalse(doc.contains("brand\":\"Tesla")); 55 | context.assertTrue(doc.contains("brand\":\"Toyota")); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | customer-service: 5 | image: java:alpine 6 | ports: 7 | - "8081:8081" 8 | volumes: 9 | - $DIR/../customer-service/target/customer-service-fat.jar:/var/app.jar:z 10 | - $DIR/../customer-service/src/main/conf/config.json:/etc/config.json:ro 11 | command: java -Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory -jar /var/app.jar -conf /etc/config.json 12 | 13 | vehicle-service: 14 | image: java:alpine 15 | ports: 16 | - "8082:8082" 17 | volumes: 18 | - $DIR/../vehicle-service/target/vehicle-service-fat.jar:/var/app.jar:z 19 | - $DIR/../vehicle-service/src/main/conf/config.json:/etc/config.json 20 | command: java -Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory -jar /var/app.jar -conf /etc/config.json 21 | 22 | rental-service: 23 | image: java:alpine 24 | ports: 25 | - "8083:8083" 26 | volumes: 27 | - $DIR/../rental-service/target/rental-service-fat.jar:/var/app.jar:z 28 | - $DIR/../rental-service/src/main/conf/config.json:/etc/config.json 29 | command: java -Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory -jar /var/app.jar -conf /etc/config.json 30 | 31 | graphql-service: 32 | image: java:alpine 33 | ports: 34 | - "8080:8080" 35 | links: 36 | - customer-service 37 | - vehicle-service 38 | - rental-service 39 | depends_on: 40 | - customer-service 41 | - vehicle-service 42 | - rental-service 43 | volumes: 44 | - $DIR/../graphql-service/target/graphql-service-fat.jar:/var/app.jar:z 45 | - $DIR/../graphql-service/src/main/conf/docker.json:/etc/config.json 46 | command: java -Dvertx.disableDnsResolver=true -Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory -jar /var/app.jar -conf /etc/config.json 47 | -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/model/QueryCustomersTest.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.model; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | import com.google.inject.Injector; 6 | import graphql.ExecutionResult; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | import static java.util.Collections.emptyMap; 14 | 15 | @RunWith(VertxUnitRunner.class) 16 | public class QueryCustomersTest extends BaseGraphQLTest { 17 | private static String query = "{ customers { id name } }"; 18 | 19 | private Injector injector; 20 | 21 | @Before 22 | public void setup(TestContext context) throws Exception { 23 | super.setup(context); 24 | injector = setupCustomersInjector(); 25 | initGraphQLAsync(injector, "QueryCustomers"); 26 | } 27 | 28 | @Test 29 | public void shouldRetrieveAllCustomers(TestContext context) throws Exception { 30 | // Given 31 | String query = "{ customers { id name } }"; 32 | 33 | // When 34 | CompletionStage future = 35 | graphQL.executeAsync(query, null, null, emptyMap()); 36 | 37 | // Then 38 | printAndValidate(future, doc -> { 39 | context.assertTrue(doc.contains("name\":\"Albert Einstein")); 40 | context.assertTrue(doc.contains("name\":\"Isaac Newton")); 41 | }); 42 | } 43 | 44 | @Test 45 | public void shouldRetrieveByCustomerId(TestContext context) throws Exception { 46 | // Given 47 | String query = "{ customer(id:2) { id name } }"; 48 | 49 | // When 50 | CompletionStage future = 51 | graphQL.executeAsync(query, null, null, emptyMap()); 52 | 53 | // Then 54 | printAndValidate(future, doc -> { 55 | context.assertFalse(doc.contains("name\":\"Albert Einstein")); 56 | context.assertTrue(doc.contains("name\":\"Isaac Newton")); 57 | }); 58 | } 59 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/resolvers/VehicleResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.resolvers; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | 6 | import com.github.bmsantos.graphql.dataloaders.DataLoaders; 7 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 8 | import com.github.bmsantos.graphql.model.vehicles.Vehicle.Unresolved; 9 | import io.engagingspaces.vertx.dataloader.DataLoader; 10 | import io.vertx.core.logging.Logger; 11 | 12 | import static com.github.bmsantos.graphql.utils.VertxCompletableFutureUtils.completedVertxCompletableFuture; 13 | import static io.vertx.core.Vertx.currentContext; 14 | import static io.vertx.core.logging.LoggerFactory.getLogger; 15 | import static java.util.Objects.requireNonNull; 16 | import static me.escoffier.vertx.completablefuture.VertxCompletableFuture.from; 17 | 18 | public class VehicleResolver implements Vehicle.AsyncResolver { 19 | private static final Logger log = getLogger(VehicleResolver.class); 20 | 21 | @Override 22 | public CompletableFuture> resolve(final List unresolved) { 23 | return null; 24 | } 25 | 26 | @Override 27 | public CompletableFuture> resolve(final Object context, final List unresolved) { 28 | if (!requireNonNull(unresolved).isEmpty()) { 29 | final Vehicle vehicle = unresolved.get(0); 30 | return processRentalVehicle(((DataLoaders)context).getVehicleDataLoader(), requireNonNull(vehicle)); 31 | } 32 | return completedVertxCompletableFuture(null); 33 | } 34 | 35 | private CompletableFuture> processRentalVehicle(final DataLoader> dataLoader, final Vehicle vehicle) { 36 | if (vehicle.getClass().equals(Unresolved.class)) { 37 | final Long id = vehicle.getId(); 38 | log.debug("Fetching vehicle for rental id: " + id); 39 | try { 40 | return from(currentContext(), dataLoader.load(id)); 41 | } catch (final Exception e) { 42 | log.error("Failed to fetch vehicle with id: " + id, e); 43 | } finally { 44 | dataLoader.dispatch(); 45 | } 46 | } 47 | return completedVertxCompletableFuture(null); 48 | } 49 | } -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/model/QueryRentalsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.model; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | import com.google.inject.Injector; 6 | import graphql.ExecutionResult; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | import static java.util.Collections.emptyMap; 14 | 15 | @RunWith(VertxUnitRunner.class) 16 | public class QueryRentalsTest extends BaseGraphQLTest { 17 | 18 | private Injector injector; 19 | 20 | @Before 21 | public void setup(TestContext context) throws Exception { 22 | super.setup(context); 23 | injector = setupRentalsInjector(); 24 | initGraphQLAsync(injector, "QueryRentals"); 25 | } 26 | 27 | @Test 28 | public void shouldRetrieveAllRentals(TestContext context) throws Exception { 29 | // Given 30 | final String query = "{ rentals { id customer { id name } vehicle { id brand }} }"; 31 | 32 | // When 33 | CompletionStage future = 34 | graphQL.executeAsync(query, null, null, emptyMap()); 35 | 36 | // Then 37 | printAndValidate(future, doc -> { 38 | context.assertTrue(doc.contains("name\":\"Albert Einstein")); 39 | context.assertTrue(doc.contains("brand\":\"Toyota")); 40 | 41 | context.assertTrue(doc.contains("name\":\"Isaac Newton")); 42 | context.assertTrue(doc.contains("brand\":\"Tesla")); 43 | }); 44 | } 45 | 46 | @Test 47 | public void shouldRetrieveRentalById(TestContext context) throws Exception { 48 | // Given 49 | final String query = "{ rental(id: 2) { id customer { id name } vehicle { id brand }} }"; 50 | 51 | // When 52 | CompletionStage future = 53 | graphQL.executeAsync(query, null, null, emptyMap()); 54 | 55 | // Then 56 | printAndValidate(future, doc -> { 57 | context.assertFalse(doc.contains("name\":\"Albert Einstein")); 58 | context.assertFalse(doc.contains("brand\":\"Toyota")); 59 | 60 | context.assertTrue(doc.contains("name\":\"Isaac Newton")); 61 | context.assertTrue(doc.contains("brand\":\"Tesla")); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/AppVerticle.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql; 2 | 3 | import javax.inject.Inject; 4 | 5 | import com.github.bmsantos.graphql.apigen.guice.AppModule; 6 | import com.github.bmsantos.graphql.apigen.resolvers.VehicleResolver; 7 | import com.github.bmsantos.graphql.model.guice.GuiceModule; 8 | import com.github.bmsantos.graphql.rest.GraphQLHandler; 9 | import com.google.inject.Injector; 10 | import io.vertx.core.AbstractVerticle; 11 | import io.vertx.core.Context; 12 | import io.vertx.core.Future; 13 | import io.vertx.core.Vertx; 14 | import io.vertx.core.http.HttpServer; 15 | import io.vertx.core.logging.Logger; 16 | import io.vertx.ext.web.Router; 17 | import io.vertx.ext.web.handler.BodyHandler; 18 | 19 | import static com.google.inject.Guice.createInjector; 20 | import static io.vertx.core.Future.future; 21 | import static io.vertx.core.logging.LoggerFactory.getLogger; 22 | import static io.vertx.ext.web.Router.router; 23 | 24 | public class AppVerticle extends AbstractVerticle { 25 | private static final Logger log = getLogger(AppVerticle.class); 26 | 27 | @Inject 28 | public GraphQLHandler graphQLHandler; 29 | 30 | @Override 31 | public void init(final Vertx vertx, final Context context) { 32 | super.init(vertx, context); 33 | } 34 | 35 | @Override 36 | public void start(final Future startFuture) throws Exception { 37 | final Injector injector = createInjector(new GuiceModule(), new AppModule()); 38 | context.put("injector", injector); 39 | injector.injectMembers(this); 40 | 41 | 42 | final Router router = router(vertx); 43 | router.route().handler(BodyHandler.create()); 44 | router.post("/graphql").handler(graphQLHandler); 45 | 46 | // vertx.eventBus() 47 | // .consumer("vehicle_publisher") 48 | // .handler(VehicleResolver::handleVehicleRequest); 49 | 50 | startHttpServer(router).setHandler(startFuture.completer()); 51 | } 52 | 53 | private Future startHttpServer(final Router router) { 54 | Future httpServerFuture = future(); 55 | vertx.createHttpServer() 56 | .requestHandler(it -> router.accept(it)) 57 | .listen(config().getInteger("service.http.port", 8080), 58 | config().getString("service.http.address", "0.0.0.0"), 59 | httpServerFuture.completer()); 60 | return httpServerFuture.map(r -> null); 61 | } 62 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/utils/UnmarshallerOperator.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.utils; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.type.TypeReference; 6 | import io.vertx.core.buffer.Buffer; 7 | import io.vertx.core.json.Json; 8 | import rx.Observable; 9 | import rx.Subscriber; 10 | 11 | import static java.util.Objects.nonNull; 12 | 13 | public abstract class UnmarshallerOperator implements Observable.Operator { 14 | 15 | private final Class mappedType; 16 | private final TypeReference mappedTypeRef; 17 | 18 | public UnmarshallerOperator(Class mappedType) { 19 | this.mappedType = mappedType; 20 | this.mappedTypeRef = null; 21 | } 22 | 23 | public UnmarshallerOperator(TypeReference mappedTypeRef) { 24 | this.mappedType = null; 25 | this.mappedTypeRef = mappedTypeRef; 26 | } 27 | 28 | public abstract Buffer unwrap(B buffer); 29 | 30 | @Override 31 | public Subscriber call(Subscriber subscriber) { 32 | final Buffer buffer = Buffer.buffer(); 33 | 34 | return new Subscriber(subscriber) { 35 | 36 | @Override 37 | public void onCompleted() { 38 | try { 39 | T obj = null; 40 | if (buffer.length() > 0) { 41 | obj = nonNull(mappedType) ? 42 | Json.mapper.readValue(buffer.getBytes(), mappedType) : 43 | Json.mapper.readValue(buffer.getBytes(), mappedTypeRef); 44 | } 45 | subscriber.onNext(obj); 46 | subscriber.onCompleted(); 47 | } catch (IOException e) { 48 | onError(e); 49 | } 50 | } 51 | 52 | @Override 53 | public void onError(Throwable e) { 54 | subscriber.onError(e); 55 | } 56 | 57 | @Override 58 | public void onNext(B item) { 59 | buffer.appendBuffer(unwrap(item)); 60 | } 61 | }; 62 | } 63 | 64 | public static Observable.Operator unmarshaller(TypeReference mappedTypeRef) { 65 | return new UnmarshallerOperator(mappedTypeRef) { 66 | @Override 67 | public Buffer unwrap(Buffer buffer) { 68 | return buffer; 69 | } 70 | }; 71 | } 72 | 73 | public static Observable.Operator unmarshaller(Class mappedType) { 74 | return new UnmarshallerOperator(mappedType) { 75 | @Override 76 | public Buffer unwrap(Buffer buffer) { 77 | return buffer; 78 | } 79 | }; 80 | } 81 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/guice/AppModule.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.guice; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.module.SimpleModule; 5 | import com.github.bmsantos.graphql.apigen.json.CustomerDeserializer; 6 | import com.github.bmsantos.graphql.apigen.json.RentalDeserializer; 7 | import com.github.bmsantos.graphql.apigen.json.VehicleDeserializer; 8 | import com.github.bmsantos.graphql.model.customer.Customer; 9 | import com.github.bmsantos.graphql.model.customer.QueryCustomers; 10 | import com.github.bmsantos.graphql.model.rental.QueryRentals; 11 | import com.github.bmsantos.graphql.model.rental.Rental; 12 | import com.github.bmsantos.graphql.model.vehicles.QueryVehicles; 13 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 14 | import com.github.bmsantos.graphql.apigen.queries.QueryCustomersImpl; 15 | import com.github.bmsantos.graphql.apigen.queries.QueryRentalsImpl; 16 | import com.github.bmsantos.graphql.apigen.queries.QueryVehiclesImpl; 17 | import com.github.bmsantos.graphql.apigen.resolvers.CustomerResolver; 18 | import com.github.bmsantos.graphql.apigen.resolvers.RentalResolver; 19 | import com.github.bmsantos.graphql.apigen.resolvers.VehicleResolver; 20 | import com.github.bmsantos.graphql.rest.GraphQLHandler; 21 | import com.github.bmsantos.graphql.rest.RestClient; 22 | import com.google.inject.AbstractModule; 23 | 24 | import static io.vertx.core.json.Json.mapper; 25 | 26 | public class AppModule extends AbstractModule { 27 | @Override 28 | protected void configure() { 29 | bind(GraphQLHandler.class); 30 | bind(RestClient.class).toInstance(new RestClient()); 31 | 32 | bind(Customer.AsyncResolver.class).toInstance(new CustomerResolver()); 33 | bind(Vehicle.AsyncResolver.class).toInstance(new VehicleResolver()); 34 | bind(Rental.AsyncResolver.class).toInstance(new RentalResolver()); 35 | 36 | bind(QueryCustomers.class).toInstance(new QueryCustomersImpl()); 37 | bind(QueryVehicles.class).toInstance(new QueryVehiclesImpl()); 38 | bind(QueryRentals.class).toInstance(new QueryRentalsImpl()); 39 | 40 | // Setup JSon mapper 41 | bind(ObjectMapper.class).toInstance(mapper); 42 | final SimpleModule module = new SimpleModule(); 43 | module.addDeserializer(Rental.class, new RentalDeserializer()); 44 | module.addDeserializer(Customer.class, new CustomerDeserializer()); 45 | module.addDeserializer(Vehicle.class, new VehicleDeserializer()); 46 | mapper.registerModule(module); 47 | } 48 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/resolvers/CustomerResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.resolvers; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | import javax.inject.Inject; 6 | 7 | import com.github.bmsantos.graphql.dataloaders.DataLoaders; 8 | import com.github.bmsantos.graphql.model.customer.Customer; 9 | import com.github.bmsantos.graphql.model.customer.Customer.Unresolved; 10 | import com.github.bmsantos.graphql.rest.RestClient; 11 | import com.google.common.collect.Lists; 12 | import io.engagingspaces.vertx.dataloader.DataLoader; 13 | import io.vertx.core.Future; 14 | import io.vertx.core.logging.Logger; 15 | 16 | import static com.github.bmsantos.graphql.utils.CompletableObserver.completableObserver; 17 | import static com.github.bmsantos.graphql.utils.VertxCompletableFutureUtils.completedVertxCompletableFuture; 18 | import static io.vertx.core.CompositeFuture.all; 19 | import static io.vertx.core.Future.future; 20 | import static io.vertx.core.Vertx.currentContext; 21 | import static io.vertx.core.logging.LoggerFactory.getLogger; 22 | import static java.util.Objects.requireNonNull; 23 | import static java.util.stream.Collectors.toList; 24 | import static me.escoffier.vertx.completablefuture.VertxCompletableFuture.from; 25 | 26 | public class CustomerResolver implements Customer.AsyncResolver { 27 | private static final Logger log = getLogger(CustomerResolver.class); 28 | 29 | @Override 30 | public CompletableFuture> resolve(final List unresolved) { 31 | return null; 32 | } 33 | 34 | @Override 35 | public CompletableFuture> resolve(final Object context, final List unresolved) { 36 | if (!requireNonNull(unresolved).isEmpty()) { 37 | final Customer customer = unresolved.get(0); 38 | return processRentalCustomer(((DataLoaders)context).getCustomerDataLoader(), requireNonNull(customer)); 39 | } 40 | return completedVertxCompletableFuture(null); 41 | } 42 | 43 | private CompletableFuture> processRentalCustomer(final DataLoader> dataLoader, final Customer customer) { 44 | if (customer.getClass().equals(Unresolved.class)) { 45 | final Long id = customer.getId(); 46 | log.debug("Fetching customer for rental id: " + id); 47 | try { 48 | return from(currentContext(), dataLoader.load(id)); 49 | } catch (final Exception e) { 50 | log.error("Failed to fetch customer with id: " + id, e); 51 | } finally { 52 | dataLoader.dispatch(); 53 | } 54 | } 55 | return completedVertxCompletableFuture(null); 56 | } 57 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/apigen/resolvers/RentalResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.apigen.resolvers; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.CompletableFuture; 5 | 6 | import com.github.bmsantos.graphql.dataloaders.DataLoaders; 7 | import com.github.bmsantos.graphql.model.rental.Rental; 8 | import com.github.bmsantos.graphql.model.rental.Rental.Unresolved; 9 | import com.github.bmsantos.graphql.rest.RestClient; 10 | import io.engagingspaces.vertx.dataloader.DataLoader; 11 | import io.vertx.core.logging.Logger; 12 | import me.escoffier.vertx.completablefuture.VertxCompletableFuture; 13 | 14 | import static com.github.bmsantos.graphql.utils.CompletableObserver.completableObserver; 15 | import static com.github.bmsantos.graphql.utils.VertxCompletableFutureUtils.completedVertxCompletableFuture; 16 | import static io.vertx.core.Vertx.currentContext; 17 | import static io.vertx.core.logging.LoggerFactory.getLogger; 18 | import static java.util.Objects.requireNonNull; 19 | import static me.escoffier.vertx.completablefuture.VertxCompletableFuture.from; 20 | 21 | public class RentalResolver implements Rental.AsyncResolver { 22 | private static final Logger log = getLogger(RentalResolver.class); 23 | 24 | @Override 25 | public CompletableFuture> resolve(final List unresolved) { 26 | return null; 27 | } 28 | 29 | @Override 30 | public CompletableFuture> resolve(final Object context, final List unresolved) { 31 | log.debug("Fetching all active rentals"); 32 | 33 | final DataLoaders dataLoaders = (DataLoaders) context; 34 | 35 | if (!requireNonNull(unresolved).isEmpty()) { 36 | final Rental rental = unresolved.get(0); 37 | return processArgumentsQuery(dataLoaders.getRentalDataLoader(), requireNonNull(rental)); 38 | } 39 | 40 | return processQueryAll(dataLoaders.getRestClient()); 41 | } 42 | 43 | private CompletableFuture> processArgumentsQuery(final DataLoader> dataLoader, final Rental rental) { 44 | if (rental.getClass().equals(Unresolved.class)) { 45 | final Long id = rental.getId(); 46 | log.debug("Fetching rental with id: " + id); 47 | try { 48 | return from(currentContext(), dataLoader.load(id)); 49 | } catch (final Exception e) { 50 | log.error("Failed to fetch rental with id: " + id, e); 51 | } finally { 52 | dataLoader.dispatch(); 53 | } 54 | } 55 | return completedVertxCompletableFuture(null); 56 | } 57 | 58 | private CompletableFuture> processQueryAll(final RestClient restClient) { 59 | final VertxCompletableFuture> future = new VertxCompletableFuture<>(); 60 | restClient.findAllRentals() 61 | .subscribe(completableObserver(future)); 62 | return future; 63 | } 64 | } -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/rest/GraphQLHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.rest; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.CompletionStage; 5 | 6 | import javax.inject.Inject; 7 | 8 | import com.github.bmsantos.graphql.dataloaders.DataLoaders; 9 | import com.github.bmsantos.graphql.engine.GraphQLEngine; 10 | import graphql.ExecutionResult; 11 | import io.vertx.core.Handler; 12 | import io.vertx.core.json.Json; 13 | import io.vertx.core.json.JsonObject; 14 | import io.vertx.core.logging.Logger; 15 | import io.vertx.ext.web.RoutingContext; 16 | 17 | import static io.vertx.core.logging.LoggerFactory.getLogger; 18 | import static java.util.Collections.emptyMap; 19 | import static java.util.Objects.isNull; 20 | import static java.util.Objects.nonNull; 21 | 22 | public class GraphQLHandler implements Handler { 23 | private static Logger log = getLogger(GraphQLHandler.class); 24 | 25 | public static final int BAD_REQUEST_SC = 400; 26 | 27 | @Inject 28 | private GraphQLEngine graphQL; 29 | 30 | @Inject 31 | private RestClient restClient; 32 | 33 | @Override 34 | public void handle(final RoutingContext ctx) { 35 | final String body = ctx.getBody().toString(); 36 | if (isNull(body)) { 37 | ctx.response().setStatusCode(BAD_REQUEST_SC).end(); 38 | } else { 39 | final JsonObject json = new JsonObject(body); 40 | 41 | final String operationName = json.getString("operationName"); 42 | final String query = json.getString("query"); 43 | final Map variables = processVariables(json); 44 | 45 | log.debug("Received graphql request: " + json.toString()); 46 | 47 | if (isNull(query)) { 48 | ctx.response().setStatusCode(BAD_REQUEST_SC).end(); 49 | } else { 50 | log.debug("Executing Query: " + json); 51 | 52 | final CompletionStage future = 53 | graphQL.engine().executeAsync(query, operationName, new DataLoaders(restClient), variables); 54 | 55 | future.handle((result, throwable) -> { 56 | final String doc = Json.encode(result); 57 | ctx.response().headers().set("Content-Type", "application/json"); 58 | ctx.response().end(doc); 59 | return this; 60 | }).toCompletableFuture(); 61 | } 62 | } 63 | } 64 | 65 | private Map processVariables(final JsonObject json) { 66 | try { 67 | return json.getJsonObject("variables").getMap(); 68 | } catch (final Throwable t) { 69 | try { 70 | final String vars = json.getString("variables"); 71 | final Map map = new JsonObject(isNull(vars) ? "{}" : vars).getMap(); 72 | if (nonNull(map)) { 73 | return map; 74 | } 75 | return emptyMap(); 76 | } catch (final Throwable tt) { 77 | return emptyMap(); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/dataloaders/DataLoaders.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.dataloaders; 2 | 3 | import java.util.List; 4 | 5 | import com.github.bmsantos.graphql.model.customer.Customer; 6 | import com.github.bmsantos.graphql.model.rental.Rental; 7 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 8 | import com.github.bmsantos.graphql.rest.RestClient; 9 | import com.google.common.collect.Lists; 10 | import io.engagingspaces.vertx.dataloader.DataLoader; 11 | import io.vertx.core.Future; 12 | 13 | import static com.github.bmsantos.graphql.utils.CompletableObserver.completableObserver; 14 | import static io.vertx.core.CompositeFuture.all; 15 | import static io.vertx.core.Future.future; 16 | import static java.util.Objects.isNull; 17 | import static java.util.stream.Collectors.toList; 18 | 19 | public class DataLoaders { 20 | 21 | private RestClient restClient; 22 | private DataLoader> customerDataLoader; 23 | private DataLoader> vehicleDataLoader; 24 | private DataLoader> rentalDataLoader; 25 | 26 | public DataLoaders(final RestClient restClient) { 27 | this.restClient = restClient; 28 | } 29 | 30 | public RestClient getRestClient() { 31 | return restClient; 32 | } 33 | 34 | public DataLoader> getCustomerDataLoader() { 35 | if (isNull(customerDataLoader)) { 36 | customerDataLoader = new DataLoader<>(keys -> { 37 | List futures = keys.stream().map(key -> { 38 | 39 | final Future> future = future(); 40 | restClient.findCustomerById(key) 41 | .map(Lists::newArrayList) 42 | .subscribe(completableObserver(future)); 43 | 44 | return future; 45 | }).collect(toList()); 46 | return all(futures); 47 | }); 48 | } 49 | return customerDataLoader; 50 | } 51 | 52 | public DataLoader> getVehicleDataLoader() { 53 | if (isNull(vehicleDataLoader)) { 54 | vehicleDataLoader = new DataLoader<>(keys -> { 55 | List futures = keys.stream().map(key -> { 56 | 57 | final Future> future = future(); 58 | restClient.findVehicleById(key) 59 | .map(Lists::newArrayList) 60 | .subscribe(completableObserver(future)); 61 | 62 | return future; 63 | }).collect(toList()); 64 | return all(futures); 65 | }); 66 | } 67 | return vehicleDataLoader; 68 | } 69 | 70 | public DataLoader> getRentalDataLoader() { 71 | if (isNull(rentalDataLoader)) { 72 | rentalDataLoader = new DataLoader<>(keys -> { 73 | List futures = keys.stream().map(key -> { 74 | 75 | final Future> future = future(); 76 | restClient.findRentalById(key) 77 | .map(Lists::newArrayList) 78 | .subscribe(completableObserver(future)); 79 | 80 | return future; 81 | }).collect(toList()); 82 | return all(futures); 83 | }); 84 | } 85 | return rentalDataLoader; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /graphql-service/src/main/java/com/github/bmsantos/graphql/rest/RestClient.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.rest; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.core.type.TypeReference; 6 | import com.github.bmsantos.graphql.model.customer.Customer; 7 | import com.github.bmsantos.graphql.model.rental.Rental; 8 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 9 | import io.vertx.core.Context; 10 | import io.vertx.core.http.HttpClient; 11 | import io.vertx.core.http.HttpClientOptions; 12 | import io.vertx.core.http.HttpClientResponse; 13 | import io.vertx.core.json.JsonObject; 14 | import io.vertx.rx.java.ObservableHandler; 15 | import io.vertx.rx.java.RxHelper; 16 | import rx.Observable; 17 | 18 | import static com.github.bmsantos.graphql.utils.UnmarshallerOperator.unmarshaller; 19 | import static io.vertx.core.Vertx.currentContext; 20 | import static io.vertx.rx.java.RxHelper.observableHandler; 21 | import static java.util.Objects.isNull; 22 | 23 | public class RestClient { 24 | private String rentalUri; 25 | private String vehicleUri; 26 | private String customerUri; 27 | 28 | public RestClient() { 29 | final JsonObject config = currentContext().config(); 30 | customerUri = config.getString("customer.service.url", "http://localhost:8081"); 31 | vehicleUri = config.getString("vehicle.service.url", "http://localhost:8082"); 32 | rentalUri = config.getString("rental.service.url", "http://localhost:8083"); 33 | } 34 | 35 | public Observable> findAllRentals() { 36 | final ObservableHandler handler = observableHandler(); 37 | getHttpClient("rentals").getAbs(rentalUri + "/rentals", handler.toHandler()).end(); 38 | return handler 39 | .flatMap(RxHelper::toObservable) 40 | .lift(unmarshaller(new TypeReference>() { })); 41 | } 42 | 43 | public Observable findRentalById(final Long id) { 44 | final ObservableHandler handler = observableHandler(); 45 | getHttpClient("rentals").getAbs(rentalUri + "/rentals/" + id, handler.toHandler()).end(); 46 | return handler 47 | .flatMap(RxHelper::toObservable) 48 | .lift(unmarshaller(Rental.class)); 49 | } 50 | 51 | public Observable findCustomerById(final Long id) { 52 | final ObservableHandler handler = observableHandler(); 53 | getHttpClient("customers").getAbs(customerUri + "/customers/" + id, handler.toHandler()).end(); 54 | return handler 55 | .flatMap(RxHelper::toObservable) 56 | .lift(unmarshaller(Customer.class)); 57 | } 58 | 59 | public Observable findVehicleById(final Long id) { 60 | final ObservableHandler handler = observableHandler(); 61 | getHttpClient("vehicles").getAbs(vehicleUri + "/vehicles/" + id, handler.toHandler()).end(); 62 | return handler 63 | .flatMap(RxHelper::toObservable) 64 | .lift(unmarshaller(Vehicle.class)); 65 | } 66 | 67 | private HttpClient getHttpClient(final String name) { 68 | final Context context = currentContext(); 69 | final String id = name + ":" + context.deploymentID(); 70 | HttpClient client = context.get(id); 71 | if (isNull(client)) { 72 | client = currentContext().owner().createHttpClient(new HttpClientOptions()); 73 | context.put(id, client); 74 | } 75 | return client; 76 | } 77 | } -------------------------------------------------------------------------------- /graphql-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | com.github.bmsantos 9 | vertx-graphql-example 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | graphql-service 14 | 15 | 16 | com.github.bmsantos.graphql.AppVerticle 17 | 18 | 19 | 20 | 21 | me.escoffier.vertx 22 | vertx-completable-future 23 | 24 | 25 | com.distelli.graphql 26 | graphql-apigen-deps 27 | 2.1.1-SNAPSHOT 28 | 29 | 30 | org.antlr 31 | ST4 32 | 4.0.8 33 | 34 | 35 | com.google.inject 36 | guice 37 | 4.0 38 | true 39 | 40 | 41 | com.google.inject.extensions 42 | guice-multibindings 43 | 4.0 44 | true 45 | 46 | 47 | io.engagingspaces 48 | vertx-dataloader 49 | 1.0.0 50 | 51 | 52 | 53 | io.vertx 54 | vertx-unit 55 | test 56 | 57 | 58 | 59 | 60 | 61 | 62 | com.distelli.graphql 63 | graphql-apigen 64 | 2.1.1-SNAPSHOT 65 | 66 | src/main/schema 67 | src/main/generated 68 | com.github.bmsantos.graphql.model 69 | com.github.bmsantos.graphql.model.guice.GuiceModule 70 | 71 | 72 | 73 | graphql-model 74 | generate-sources 75 | 76 | apigen 77 | 78 | 79 | 80 | 81 | 82 | maven-shade-plugin 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vert.x GraphQL Example 2 | 3 | ![VG](https://raw.githubusercontent.com/bmsantos/vertx-graphql-example/master/vertx-graphql-mic-drop.png) 4 | 5 | When it comes to performance and scalability, Vert.x has always been hard to beat and version 3 just made it much easier to develop and deploy. 6 | 7 | This simple application is used to demonstrate: 8 | 9 | - that Java CompletableFuture, Vert.x Futures and RxJava can be easily combined 10 | - that Vert.x micro-services are easy to develop and deploy through Docker containers 11 | 12 | The goal of this application is to exercise graphql-java async (non-blocking) with Vert.x. 13 | 14 | In addition it also uses: 15 | 16 | - [graphql-apigen](https://github.com/bmsantos/graphql-apigen/tree/async) - to facilitate the graphql schema generation 17 | - [vertx-dataloader](https://github.com/engagingspaces/vertx-dataloader) - to ensure a consistent API data fetching between the different resources 18 | 19 | 20 | ## System Architecture 21 | 22 | ```text 23 | .---------. .-----------. 24 | POST /graphql --> | GraphQL | | Customer | 25 | | Service | ----> | Service | 26 | '---------' | '-----------' 27 | | .-----------. 28 | | | Vehicle | 29 | |-> | Service | 30 | | '-----------' 31 | | .-----------. 32 | | | Rental | 33 | '-> | Service | 34 | '-----------' 35 | ``` 36 | 37 | 38 | ## Before you start 39 | 40 | ```graphql-java-async``` is not out yet. In order to build this project you need to: 41 | 42 | 1. ```graphql-java``` - Checkout and build Dmitry's [async branch](https://github.com/dminkovsky/graphql-java/tree/async) 43 | 1. ```graphql-apigen``` - Checkout and build the [eb_graphql branch](https://github.com/bmsantos/graphql-apigen/tree/eb_graphql) of my fork of [Distelli/graphql-apigen](https://github.com/Distelli/graphql-apigen) 44 | 45 | ## Build: 46 | 47 | After building the async branches of both graphql-java and graphql-apigen do: 48 | 49 | ```sh 50 | mvn clean package 51 | ``` 52 | 53 | 54 | ## Execute: 55 | 56 | ```sh 57 | ./docker/run.sh 58 | ``` 59 | 60 | 61 | ## Test 62 | 63 | The graphql-service exposes a POST endpoint. You can use CURL but it is recommended to use [Graphiql App](https://github.com/skevy/graphiql-app). 64 | 65 | Sample queries to use on a POST to http://localhost:8080/graphql. 66 | 67 | 68 | ### Querying for a single rental entry: 69 | ```graphql 70 | { 71 | rental(id: 1) { 72 | id 73 | customer { 74 | id 75 | name 76 | address 77 | city 78 | state 79 | country 80 | contact { 81 | phone 82 | type 83 | } 84 | } 85 | vehicle { 86 | id 87 | brand 88 | model 89 | type 90 | year 91 | mileage 92 | extras 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | 99 | ### Querying for all active rentals: 100 | ```graphql 101 | { 102 | rentals { 103 | id 104 | customer { 105 | id 106 | name 107 | address 108 | city 109 | state 110 | country 111 | contact { 112 | phone 113 | type 114 | } 115 | } 116 | vehicle { 117 | id 118 | brand 119 | model 120 | type 121 | year 122 | mileage 123 | extras 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | 130 | ### Example using CURL: 131 | ```bash 132 | curl -k -X POST -d '{ "operationName": null, "query": "{ rentals { customer { name } vehicle { brand model } } }", "variables": "{}" }' http://localhost:8080/graphql 133 | ``` 134 | -------------------------------------------------------------------------------- /graphql-service/src/test/java/com/github/bmsantos/graphql/model/BaseGraphQLTest.java: -------------------------------------------------------------------------------- 1 | package com.github.bmsantos.graphql.model; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import java.util.concurrent.CompletionStage; 8 | import java.util.function.Consumer; 9 | 10 | import com.github.bmsantos.graphql.model.customer.Customer; 11 | import com.github.bmsantos.graphql.model.customer.QueryCustomers; 12 | import com.github.bmsantos.graphql.model.guice.GuiceModule; 13 | import com.github.bmsantos.graphql.model.rental.QueryRentals; 14 | import com.github.bmsantos.graphql.model.rental.Rental; 15 | import com.github.bmsantos.graphql.model.vehicles.MutateVehicles; 16 | import com.github.bmsantos.graphql.model.vehicles.QueryVehicles; 17 | import com.github.bmsantos.graphql.model.vehicles.Vehicle; 18 | import com.github.bmsantos.graphql.apigen.queries.MutateVehiclesImpl; 19 | import com.github.bmsantos.graphql.apigen.queries.QueryCustomersImpl; 20 | import com.github.bmsantos.graphql.apigen.queries.QueryRentalsImpl; 21 | import com.github.bmsantos.graphql.apigen.queries.QueryVehiclesImpl; 22 | import com.github.bmsantos.graphql.apigen.resolvers.TestableCustomerResolver; 23 | import com.github.bmsantos.graphql.apigen.resolvers.TestableRentalResolver; 24 | import com.github.bmsantos.graphql.apigen.resolvers.TestableVehicleResolver; 25 | import com.github.bmsantos.graphql.utils.VertxCompletableFutureFactory; 26 | import com.google.inject.AbstractModule; 27 | import com.google.inject.Injector; 28 | import com.google.inject.Key; 29 | import com.google.inject.TypeLiteral; 30 | import graphql.ExecutionResult; 31 | import graphql.GraphQLAsync; 32 | import graphql.schema.GraphQLObjectType; 33 | import graphql.schema.GraphQLSchema; 34 | import graphql.schema.GraphQLType; 35 | import io.vertx.core.Vertx; 36 | import io.vertx.core.json.Json; 37 | import io.vertx.ext.unit.TestContext; 38 | import io.vertx.ext.unit.junit.RunTestOnContext; 39 | import org.junit.After; 40 | import org.junit.Before; 41 | import org.junit.Rule; 42 | 43 | import static com.google.inject.Guice.createInjector; 44 | import static graphql.execution.async.AsyncExecutionStrategy.parallel; 45 | 46 | abstract public class BaseGraphQLTest { 47 | 48 | protected Vertx vertx; 49 | protected GraphQLAsync graphQL; 50 | 51 | @Rule 52 | public RunTestOnContext rule = new RunTestOnContext(); 53 | 54 | @Before 55 | public void setup(TestContext context) throws Exception { 56 | vertx = rule.vertx(); 57 | } 58 | 59 | @After 60 | public void tearDown(TestContext context) { 61 | vertx.close(context.asyncAssertSuccess()); 62 | } 63 | 64 | protected void initGraphQLAsync(final Injector injector, final String queryName) { 65 | Map types = 66 | injector.getInstance(Key.get(new TypeLiteral>() { 67 | })); 68 | 69 | GraphQLSchema schema = GraphQLSchema.newSchema() 70 | .query((GraphQLObjectType) types.get(queryName)) 71 | .build(new HashSet<>(types.values())); 72 | graphQL = new GraphQLAsync(schema, parallel(new VertxCompletableFutureFactory())); 73 | } 74 | 75 | protected void initGraphQLAsync(final Injector injector, final String queryName, final String mutationName) { 76 | Map types = 77 | injector.getInstance(Key.get(new TypeLiteral>() { 78 | })); 79 | 80 | GraphQLSchema schema = GraphQLSchema.newSchema() 81 | .query((GraphQLObjectType) types.get(queryName)) 82 | .mutation((GraphQLObjectType) types.get(mutationName)) 83 | .build(new HashSet<>(types.values())); 84 | graphQL = new GraphQLAsync(schema, parallel(new VertxCompletableFutureFactory()), parallel(new VertxCompletableFutureFactory())); 85 | } 86 | 87 | protected void printAndValidate(CompletionStage future, Consumer validator) { 88 | future.handle((result, throwable) -> { 89 | String doc = Json.encode(result); 90 | System.out.println(doc); 91 | validator.accept(doc); 92 | return this; 93 | }).toCompletableFuture(); 94 | } 95 | 96 | protected Map setupVehiclesDS() { 97 | Map vehicles = new LinkedHashMap<>(); 98 | vehicles.put(1L, 99 | new Vehicle.Builder() 100 | .withId(1L) 101 | .withBrand("Toyota") 102 | .withModel("Corolla") 103 | .withType("Car") 104 | .withYear(2006) 105 | .withMileage(40000L) 106 | .build()); 107 | 108 | vehicles.put(2L, 109 | new Vehicle.Builder() 110 | .withId(2L) 111 | .withBrand("Tesla") 112 | .withModel("P100D") 113 | .withType("Car") 114 | .withYear(20017) 115 | .withMileage(100L) 116 | .build()); 117 | 118 | return vehicles; 119 | } 120 | 121 | private Map setupCustomersDS() { 122 | Map customers = new LinkedHashMap<>(); 123 | customers.put(1L, 124 | new Customer.Builder() 125 | .withId(1L) 126 | .withName("Albert Einstein") 127 | .withAddress("123 Someplace") 128 | .withCity("Some city") 129 | .withState("New York") 130 | .withCountry("USA") 131 | .build()); 132 | 133 | customers.put(2L, 134 | new Customer.Builder() 135 | .withId(2L) 136 | .withName("Isaac Newton") 137 | .withAddress("456 Somewhere Else") 138 | .withCity("Some other city") 139 | .withState("Virginia") 140 | .withCountry("USA") 141 | .build()); 142 | return customers; 143 | } 144 | 145 | private Map setupRentalsDS() { 146 | Map rentals = new HashMap<>(); 147 | rentals.put(1L, 148 | new Rental.Builder() 149 | .withId(1L) 150 | .withCustomer(new Customer.Unresolved(1L)) 151 | .withVehicle(new Vehicle.Unresolved(1L)) 152 | .build()); 153 | 154 | rentals.put(2L, 155 | new Rental.Builder() 156 | .withId(2L) 157 | .withCustomer(new Customer.Unresolved(2L)) 158 | .withVehicle(new Vehicle.Unresolved(2L)) 159 | .build()); 160 | return rentals; 161 | } 162 | 163 | public Injector setupVehiclesInjector() throws Exception { 164 | return createInjector( 165 | new GuiceModule(), 166 | new AbstractModule() { 167 | @Override 168 | protected void configure() { 169 | bind(Vehicle.AsyncResolver.class).toInstance(new TestableVehicleResolver(setupVehiclesDS())); 170 | bind(QueryVehicles.class).toInstance(new QueryVehiclesImpl()); 171 | bind(MutateVehicles.class).toInstance(new MutateVehiclesImpl()); 172 | } 173 | }); 174 | } 175 | 176 | public Injector setupCustomersInjector() throws Exception { 177 | return createInjector( 178 | new GuiceModule(), 179 | new AbstractModule() { 180 | @Override 181 | protected void configure() { 182 | bind(Customer.AsyncResolver.class).toInstance(new TestableCustomerResolver(setupCustomersDS())); 183 | bind(QueryCustomers.class).toInstance(new QueryCustomersImpl()); 184 | } 185 | }); 186 | } 187 | 188 | public Injector setupRentalsInjector() throws Exception { 189 | return createInjector( 190 | new GuiceModule(), 191 | new AbstractModule() { 192 | @Override 193 | protected void configure() { 194 | bind(Customer.AsyncResolver.class).toInstance(new TestableCustomerResolver(setupCustomersDS())); 195 | bind(Vehicle.AsyncResolver.class).toInstance(new TestableVehicleResolver(setupVehiclesDS())); 196 | bind(Rental.AsyncResolver.class).toInstance(new TestableRentalResolver(setupRentalsDS())); 197 | bind(QueryCustomers.class).toInstance(new QueryCustomersImpl()); 198 | bind(QueryVehicles.class).toInstance(new QueryVehiclesImpl()); 199 | bind(QueryRentals.class).toInstance(new QueryRentalsImpl()); 200 | } 201 | }); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.bmsantos 8 | vertx-graphql-example 9 | 0.0.1-SNAPSHOT 10 | pom 11 | 12 | 13 | vehicle-service 14 | customer-service 15 | rental-service 16 | graphql-service 17 | 18 | 19 | 20 | 3.4.0 21 | 22 | 23 | 24 | 25 | 26 | 27 | io.vertx 28 | vertx-dependencies 29 | ${vertx.version} 30 | pom 31 | import 32 | 33 | 34 | me.escoffier.vertx 35 | vertx-completable-future 36 | 0.1.1 37 | 38 | 39 | 40 | 41 | 42 | 43 | io.vertx 44 | vertx-core 45 | 46 | 47 | io.vertx 48 | vertx-rx-java 49 | 50 | 51 | io.vertx 52 | vertx-service-proxy 53 | 54 | 55 | io.vertx 56 | vertx-lang-js 57 | 58 | 59 | io.vertx 60 | vertx-lang-ruby 61 | provided 62 | 63 | 64 | io.vertx 65 | vertx-codegen 66 | provided 67 | 68 | 69 | io.vertx 70 | vertx-sockjs-service-proxy 71 | 72 | 73 | 74 | io.vertx 75 | vertx-web 76 | 77 | 78 | io.vertx 79 | vertx-hazelcast 80 | 81 | 82 | 83 | 84 | io.vertx 85 | vertx-service-discovery 86 | 87 | 88 | io.vertx 89 | vertx-circuit-breaker 90 | 91 | 92 | 93 | 94 | org.slf4j 95 | slf4j-api 96 | 1.7.12 97 | 98 | 99 | org.apache.logging.log4j 100 | log4j-api 101 | 2.3 102 | 103 | 104 | org.apache.logging.log4j 105 | log4j-core 106 | 2.3 107 | 108 | 109 | org.apache.logging.log4j 110 | log4j-slf4j-impl 111 | 2.3 112 | 113 | 114 | 115 | 116 | io.vertx 117 | vertx-unit 118 | test 119 | 120 | 121 | junit 122 | junit 123 | 4.12 124 | test 125 | 126 | 127 | org.assertj 128 | assertj-core 129 | 3.3.0 130 | test 131 | 132 | 133 | com.jayway.awaitility 134 | awaitility 135 | 1.7.0 136 | test 137 | 138 | 139 | io.rest-assured 140 | rest-assured 141 | 3.0.0 142 | test 143 | 144 | 145 | 146 | 147 | 148 | 149 | maven-compiler-plugin 150 | 151 | 1.8 152 | 1.8 153 | src/main/generated 154 | 155 | 156 | 157 | default-compile 158 | 159 | 160 | io.vertx.codegen.CodeGenProcessor 161 | 162 | src/main/generated 163 | 164 | -AoutputDirectory=${project.basedir}/src/main 165 | 166 | 167 | 168 | 169 | default-testCompile 170 | 171 | 172 | io.vertx.codegen.CodeGenProcessor 173 | 174 | src/test/generated 175 | 176 | -AoutputDirectory=${project.basedir}/src/test 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | maven-compiler-plugin 188 | 3.1 189 | 190 | 1.8 191 | 1.8 192 | 193 | 194 | 195 | 196 | org.apache.maven.plugins 197 | maven-shade-plugin 198 | 2.3 199 | 200 | 201 | package 202 | 203 | shade 204 | 205 | 206 | 207 | 209 | 210 | io.vertx.core.Launcher 211 | ${main.verticle} 212 | 213 | 214 | 216 | META-INF/services/io.vertx.core.spi.VerticleFactory 217 | 218 | 219 | 220 | 221 | ${project.build.directory}/${project.artifactId}-fat.jar 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | snapshots-repo 233 | https://oss.sonatype.org/content/repositories/snapshots 234 | 235 | false 236 | 237 | 238 | true 239 | 240 | 241 | 242 | 243 | false 244 | 245 | central 246 | bintray 247 | http://jcenter.bintray.com 248 | 249 | 250 | 251 | 252 | --------------------------------------------------------------------------------