diff --git a/settings.gradle b/settings.gradle index d891e841..697f1b0f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'spring-graphql' include 'spring-graphql-boot-starter-webmvc' +include 'spring-graphql-boot-starter-webflux' include 'spring-graphql-webmvc' +include 'spring-graphql-webflux' diff --git a/spring-graphql-boot-starter-webflux/build.gradle b/spring-graphql-boot-starter-webflux/build.gradle new file mode 100644 index 00000000..e2e693a5 --- /dev/null +++ b/spring-graphql-boot-starter-webflux/build.gradle @@ -0,0 +1,9 @@ +description = "GraphQL Java Spring Boot starter Webmvc" +dependencies { + implementation "org.springframework.boot:spring-boot-autoconfigure:$springBootVersion" + implementation project(':spring-graphql-webflux') + + testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion" + testCompile "org.springframework.boot:spring-boot-starter-webflux:$springBootVersion" + +} diff --git a/spring-graphql-boot-starter-webflux/src/main/java/org/springframework/graphql/reactive/GraphQLEndpointConfiguration.java b/spring-graphql-boot-starter-webflux/src/main/java/org/springframework/graphql/reactive/GraphQLEndpointConfiguration.java new file mode 100644 index 00000000..1a410f38 --- /dev/null +++ b/spring-graphql-boot-starter-webflux/src/main/java/org/springframework/graphql/reactive/GraphQLEndpointConfiguration.java @@ -0,0 +1,23 @@ +package org.springframework.graphql.reactive; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.reactive.components.GraphQLController; + +import javax.annotation.PostConstruct; + +@Configuration +@ConditionalOnWebApplication +@ComponentScan(basePackageClasses = GraphQLController.class) +public class GraphQLEndpointConfiguration { + + @Autowired + ApplicationContext applicationContext; + + @PostConstruct + public void init() { + } +} diff --git a/spring-graphql-boot-starter-webflux/src/main/resources/META-INF/spring.factories b/spring-graphql-boot-starter-webflux/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..98d6f33e --- /dev/null +++ b/spring-graphql-boot-starter-webflux/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.graphql.reactive.GraphQLEndpointConfiguration diff --git a/spring-graphql-boot-starter-webflux/src/test/java/org/springframework/graphql/reactive/IntegrationTest.java b/spring-graphql-boot-starter-webflux/src/test/java/org/springframework/graphql/reactive/IntegrationTest.java new file mode 100644 index 00000000..f1733ae2 --- /dev/null +++ b/spring-graphql-boot-starter-webflux/src/test/java/org/springframework/graphql/reactive/IntegrationTest.java @@ -0,0 +1,56 @@ +package org.springframework.graphql.reactive; + +import graphql.ExecutionInput; +import graphql.ExecutionResultImpl; +import graphql.GraphQL; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.BodyInserters; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ExtendWith(SpringExtension.class) +public class IntegrationTest { + + @Autowired + WebTestClient webClient; + + @Autowired + GraphQL graphql; + + @Test + public void endpointIsAvailable() { + String query = "{foo}"; + + ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult() + .data("bar") + .build(); + CompletableFuture cf = CompletableFuture.completedFuture(executionResult); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionInput.class); + Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); + + Map body = new LinkedHashMap<>(); + body.put("query", query); + + Map expectedResult = new LinkedHashMap<>(); + expectedResult.put("data", "bar"); + + this.webClient.post().uri("/graphql").body(BodyInserters.fromValue(body)).exchange().expectStatus().isOk() + .expectBody(Map.class).isEqualTo(expectedResult); + + assertThat(captor.getValue().getQuery()).isEqualTo(query); + } + +} \ No newline at end of file diff --git a/spring-graphql-boot-starter-webflux/src/test/java/org/springframework/graphql/reactive/IntegrationTestConfig.java b/spring-graphql-boot-starter-webflux/src/test/java/org/springframework/graphql/reactive/IntegrationTestConfig.java new file mode 100644 index 00000000..6398bf94 --- /dev/null +++ b/spring-graphql-boot-starter-webflux/src/test/java/org/springframework/graphql/reactive/IntegrationTestConfig.java @@ -0,0 +1,18 @@ +package org.springframework.graphql.reactive; + +import graphql.GraphQL; +import org.mockito.Mockito; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class IntegrationTestConfig { + + + @Bean + public GraphQL graphQL() { + return Mockito.mock(GraphQL.class); + } + +} + diff --git a/spring-graphql-boot-starter-webmvc/src/main/java/org/springframework/graphql/GraphQLEndpointConfiguration.java b/spring-graphql-boot-starter-webmvc/src/main/java/org/springframework/graphql/servlet/GraphQLEndpointConfiguration.java similarity index 84% rename from spring-graphql-boot-starter-webmvc/src/main/java/org/springframework/graphql/GraphQLEndpointConfiguration.java rename to spring-graphql-boot-starter-webmvc/src/main/java/org/springframework/graphql/servlet/GraphQLEndpointConfiguration.java index a5c7ee07..36ead34e 100644 --- a/spring-graphql-boot-starter-webmvc/src/main/java/org/springframework/graphql/GraphQLEndpointConfiguration.java +++ b/spring-graphql-boot-starter-webmvc/src/main/java/org/springframework/graphql/servlet/GraphQLEndpointConfiguration.java @@ -1,11 +1,11 @@ -package org.springframework.graphql; +package org.springframework.graphql.servlet; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.graphql.components.GraphQLController; +import org.springframework.graphql.servlet.components.GraphQLController; import javax.annotation.PostConstruct; diff --git a/spring-graphql-boot-starter-webmvc/src/main/resources/META-INF/spring.factories b/spring-graphql-boot-starter-webmvc/src/main/resources/META-INF/spring.factories index fba5f62c..5671ae1a 100644 --- a/spring-graphql-boot-starter-webmvc/src/main/resources/META-INF/spring.factories +++ b/spring-graphql-boot-starter-webmvc/src/main/resources/META-INF/spring.factories @@ -1,2 +1,2 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.graphql.GraphQLEndpointConfiguration +org.springframework.graphql.servlet.GraphQLEndpointConfiguration diff --git a/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/IntegrationTest.java b/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/servlet/IntegrationTest.java similarity index 97% rename from spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/IntegrationTest.java rename to spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/servlet/IntegrationTest.java index ca2f95ec..aa7c8e90 100644 --- a/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/IntegrationTest.java +++ b/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/servlet/IntegrationTest.java @@ -1,4 +1,4 @@ -package org.springframework.graphql; +package org.springframework.graphql.servlet; import graphql.ExecutionInput; import graphql.ExecutionResultImpl; diff --git a/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/IntegrationTestConfig.java b/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/servlet/IntegrationTestConfig.java similarity index 88% rename from spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/IntegrationTestConfig.java rename to spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/servlet/IntegrationTestConfig.java index b6862103..5a40dcb2 100644 --- a/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/IntegrationTestConfig.java +++ b/spring-graphql-boot-starter-webmvc/src/test/java/org/springframework/graphql/servlet/IntegrationTestConfig.java @@ -1,4 +1,4 @@ -package org.springframework.graphql; +package org.springframework.graphql.servlet; import graphql.GraphQL; import org.mockito.Mockito; diff --git a/spring-graphql-webflux/build.gradle b/spring-graphql-webflux/build.gradle new file mode 100644 index 00000000..2e554b59 --- /dev/null +++ b/spring-graphql-webflux/build.gradle @@ -0,0 +1,16 @@ +description = "GraphQL Java Spring Webmvc integration" + +apply plugin: 'java-library' + +dependencies { + implementation "org.springframework:spring-webflux:$springVersion" + implementation "org.springframework:spring-context:$springVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + api "com.graphql-java:graphql-java:$graphqlJavaVersion" + + testImplementation("org.assertj:assertj-core:$assertJVersion") + testImplementation('org.junit.jupiter:junit-jupiter:5.6.2') + testImplementation "org.springframework:spring-test:$springVersion" + testImplementation group: 'com.jayway.jsonpath', name: 'json-path', version: '2.4.0' + testImplementation "org.mockito:mockito-core:2.+" +} diff --git a/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/GraphQLInvocationData.java b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/GraphQLInvocationData.java new file mode 100644 index 00000000..76e80a8d --- /dev/null +++ b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/GraphQLInvocationData.java @@ -0,0 +1,31 @@ +package org.springframework.graphql.reactive; + +import graphql.Assert; + +import java.util.Collections; +import java.util.Map; + +public class GraphQLInvocationData { + + private final String query; + private final String operationName; + private final Map variables; + + public GraphQLInvocationData(String query, String operationName, Map variables) { + this.query = Assert.assertNotNull(query, () -> "query must be provided"); + this.operationName = operationName; + this.variables = variables != null ? variables : Collections.emptyMap(); + } + + public String getQuery() { + return query; + } + + public String getOperationName() { + return operationName; + } + + public Map getVariables() { + return variables; + } +} diff --git a/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLController.java b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLController.java new file mode 100644 index 00000000..5492a887 --- /dev/null +++ b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLController.java @@ -0,0 +1,45 @@ +package org.springframework.graphql.reactive.components; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.graphql.reactive.GraphQLInvocationData; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import java.util.Map; + +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +@Component +public class GraphQLController { + + @Autowired + GraphQLRequestHandler graphQLRequestHandler; + + @Bean + public RouterFunction routerFunction() { + RouterFunction route = route() + .POST("/graphql", this::graphqlPOST) + .build(); + return route; + + } + + private Mono graphqlPOST(ServerRequest serverRequest) { + Mono bodyMono = serverRequest.bodyToMono(GraphQLRequestBody.class); + return bodyMono.flatMap(body -> { + String query = body.getQuery(); + if (query == null) { + query = ""; + } + GraphQLInvocationData invocationData = new GraphQLInvocationData(query, body.getOperationName(), body.getVariables()); + Mono resultBodyMono = graphQLRequestHandler.invoke(invocationData, serverRequest.headers()); + return resultBodyMono.flatMap(resultBody -> ServerResponse.ok().bodyValue(resultBody)); + }); + } + +} diff --git a/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLRequestBody.java b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLRequestBody.java new file mode 100644 index 00000000..a07b2f95 --- /dev/null +++ b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLRequestBody.java @@ -0,0 +1,33 @@ +package org.springframework.graphql.reactive.components; + +import java.util.Map; + +public class GraphQLRequestBody { + private String query; + private String operationName; + private Map variables; + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getOperationName() { + return operationName; + } + + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + public Map getVariables() { + return variables; + } + + public void setVariables(Map variables) { + this.variables = variables; + } +} diff --git a/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLRequestHandler.java b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLRequestHandler.java new file mode 100644 index 00000000..915ae7a2 --- /dev/null +++ b/spring-graphql-webflux/src/main/java/org/springframework/graphql/reactive/components/GraphQLRequestHandler.java @@ -0,0 +1,44 @@ +package org.springframework.graphql.reactive.components; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.graphql.reactive.GraphQLInvocationData; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.reactive.function.server.ServerRequest; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@Component +public class GraphQLRequestHandler { + + @Autowired + private GraphQL graphQL; + + public Mono invoke(GraphQLInvocationData invocationData, + ServerRequest.Headers headers) { + Assert.notNull(graphQL, "graphQL is not set"); + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(invocationData.getQuery()) + .operationName(invocationData.getOperationName()) + .variables(invocationData.getVariables()) + .build(); + customizeExecutionInput(executionInput, headers); + CompletableFuture customizedExecutionInput = customizeExecutionInput(executionInput, headers); + CompletableFuture executionResultCompletableFuture = customizedExecutionInput.thenCompose(graphQL::executeAsync); + return handleExecutionResult(executionResultCompletableFuture); + } + + protected CompletableFuture customizeExecutionInput(ExecutionInput executionInput, + ServerRequest.Headers headers) { + return CompletableFuture.completedFuture(executionInput); + } + + protected Mono handleExecutionResult(CompletableFuture executionResultCF) { + return Mono.fromCompletionStage(executionResultCF).map(ExecutionResult::toSpecification); + } +} diff --git a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/GraphQLInvocationData.java b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/GraphQLInvocationData.java similarity index 94% rename from spring-graphql-webmvc/src/main/java/org/springframework/graphql/GraphQLInvocationData.java rename to spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/GraphQLInvocationData.java index 42c3e53e..dab716cd 100644 --- a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/GraphQLInvocationData.java +++ b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/GraphQLInvocationData.java @@ -1,4 +1,4 @@ -package org.springframework.graphql; +package org.springframework.graphql.servlet; import graphql.Assert; diff --git a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLController.java b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLController.java similarity index 92% rename from spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLController.java rename to spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLController.java index 914bea96..5e353b26 100644 --- a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLController.java +++ b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLController.java @@ -1,9 +1,9 @@ -package org.springframework.graphql.components; +package org.springframework.graphql.servlet.components; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; -import org.springframework.graphql.GraphQLInvocationData; +import org.springframework.graphql.servlet.GraphQLInvocationData; import org.springframework.stereotype.Component; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerRequest; diff --git a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLRequestBody.java b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLRequestBody.java similarity index 92% rename from spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLRequestBody.java rename to spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLRequestBody.java index 2cb1e6e3..666f08b3 100644 --- a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLRequestBody.java +++ b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLRequestBody.java @@ -1,4 +1,4 @@ -package org.springframework.graphql.components; +package org.springframework.graphql.servlet.components; import java.util.Map; diff --git a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLRequestHandler.java b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLRequestHandler.java similarity index 94% rename from spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLRequestHandler.java rename to spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLRequestHandler.java index dd01bd50..2eb29f44 100644 --- a/spring-graphql-webmvc/src/main/java/org/springframework/graphql/components/GraphQLRequestHandler.java +++ b/spring-graphql-webmvc/src/main/java/org/springframework/graphql/servlet/components/GraphQLRequestHandler.java @@ -1,10 +1,10 @@ -package org.springframework.graphql.components; +package org.springframework.graphql.servlet.components; import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.GraphQL; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.graphql.GraphQLInvocationData; +import org.springframework.graphql.servlet.GraphQLInvocationData; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.web.servlet.function.ServerRequest;