diff --git a/build.gradle b/build.gradle index 0c07156..38b013f 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,7 @@ subprojects { springVersion = "5.1.2.RELEASE" springBootVersion = "2.1.0.RELEASE" jacksonVersion = "2.9.6" + assertJVersion = "3.11.1" } repositories { diff --git a/graphql-java-spring-webflux/build.gradle b/graphql-java-spring-webflux/build.gradle index 00601bf..903398e 100644 --- a/graphql-java-spring-webflux/build.gradle +++ b/graphql-java-spring-webflux/build.gradle @@ -6,6 +6,7 @@ dependencies { compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" compile "com.graphql-java:graphql-java:$graphqlJavaVersion" + testCompile("org.assertj:assertj-core:$assertJVersion") testCompile group: 'junit', name: 'junit', version: '4.12' testCompile "org.springframework:spring-test:$springVersion" testCompile group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1' diff --git a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/ExecutionInputCustomizer.java b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/ExecutionInputCustomizer.java new file mode 100644 index 0000000..09712df --- /dev/null +++ b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/ExecutionInputCustomizer.java @@ -0,0 +1,19 @@ +package graphql.spring.web.reactive; + +import graphql.ExecutionInput; +import graphql.PublicApi; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * Lets you customize the #ExecutionInput before the query is executed. + * You can for example set a context object or define a root value. + *

+ * This is only used if you use the default {@link GraphQLInvocation}. + */ +@PublicApi +public interface ExecutionInputCustomizer { + + Mono customizeExecutionInput(ExecutionInput executionInput, ServerWebExchange webRequest); + +} diff --git a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/GraphQLInvocation.java b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/GraphQLInvocation.java index 0e69810..78664b9 100644 --- a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/GraphQLInvocation.java +++ b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/GraphQLInvocation.java @@ -8,6 +8,6 @@ @PublicApi public interface GraphQLInvocation { - Mono invoke(GraphQLInvocationData invocationData, ServerWebExchange webRequest); + Mono invoke(GraphQLInvocationData invocationData, ServerWebExchange serverWebExchange); } diff --git a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/DefaultExecutionInputCustomizer.java b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/DefaultExecutionInputCustomizer.java new file mode 100644 index 0000000..05732b9 --- /dev/null +++ b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/DefaultExecutionInputCustomizer.java @@ -0,0 +1,18 @@ +package graphql.spring.web.reactive.components; + +import graphql.ExecutionInput; +import graphql.Internal; +import graphql.spring.web.reactive.ExecutionInputCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Component +@Internal +public class DefaultExecutionInputCustomizer implements ExecutionInputCustomizer { + + @Override + public Mono customizeExecutionInput(ExecutionInput executionInput, ServerWebExchange webRequest) { + return Mono.just(executionInput); + } +} diff --git a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/DefaultGraphQLInvocation.java b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/DefaultGraphQLInvocation.java index 5d0c5a0..196c835 100644 --- a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/DefaultGraphQLInvocation.java +++ b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/DefaultGraphQLInvocation.java @@ -4,6 +4,7 @@ import graphql.ExecutionResult; import graphql.GraphQL; import graphql.Internal; +import graphql.spring.web.reactive.ExecutionInputCustomizer; import graphql.spring.web.reactive.GraphQLInvocation; import graphql.spring.web.reactive.GraphQLInvocationData; import org.springframework.beans.factory.annotation.Autowired; @@ -16,7 +17,10 @@ public class DefaultGraphQLInvocation implements GraphQLInvocation { @Autowired - private GraphQL graphQL; + GraphQL graphQL; + + @Autowired + ExecutionInputCustomizer executionInputCustomizer; @Override public Mono invoke(GraphQLInvocationData invocationData, ServerWebExchange serverWebExchange) { @@ -25,7 +29,8 @@ public Mono invoke(GraphQLInvocationData invocationData, Server .operationName(invocationData.getOperationName()) .variables(invocationData.getVariables()) .build(); - return Mono.fromCompletionStage(graphQL.executeAsync(executionInput)); + Mono customizedExecutionInputMono = executionInputCustomizer.customizeExecutionInput(executionInput, serverWebExchange); + return customizedExecutionInputMono.flatMap(customizedExecutionInput -> Mono.fromCompletionStage(graphQL.executeAsync(customizedExecutionInput))); } } diff --git a/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DefaultGraphQLInvocationTest.java b/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DefaultGraphQLInvocationTest.java new file mode 100644 index 0000000..2e3d9ef --- /dev/null +++ b/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DefaultGraphQLInvocationTest.java @@ -0,0 +1,62 @@ +package graphql.spring.web.reactive.components; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.spring.web.reactive.ExecutionInputCustomizer; +import graphql.spring.web.reactive.GraphQLInvocationData; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DefaultGraphQLInvocationTest { + + + @Test + public void testCustomizerIsCalled() { + + String query = "query myQuery {foo}"; + String operationName = "myQuery"; + Map variables = new LinkedHashMap<>(); + + DefaultGraphQLInvocation defaultGraphQLInvocation = new DefaultGraphQLInvocation(); + ExecutionInputCustomizer executionInputCustomizer = mock(ExecutionInputCustomizer.class); + defaultGraphQLInvocation.executionInputCustomizer = executionInputCustomizer; + GraphQL graphQL = mock(GraphQL.class); + defaultGraphQLInvocation.graphQL = graphQL; + ExecutionResult executionResult = mock(ExecutionResult.class); + when(graphQL.executeAsync(any(ExecutionInput.class))).thenReturn(completedFuture(executionResult)); + + GraphQLInvocationData graphQLInvocationData = new GraphQLInvocationData(query, operationName, variables); + ServerWebExchange serverWebExchange = mock(ServerWebExchange.class); + + ArgumentCaptor captor1 = ArgumentCaptor.forClass(ExecutionInput.class); + ArgumentCaptor captor2 = ArgumentCaptor.forClass(ServerWebExchange.class); + ExecutionInput executionInputResult = mock(ExecutionInput.class); + when(executionInputCustomizer.customizeExecutionInput(captor1.capture(), captor2.capture())).thenReturn(Mono.just(executionInputResult)); + + Mono invoke = defaultGraphQLInvocation.invoke(graphQLInvocationData, serverWebExchange); + + assertThat(captor1.getValue().getQuery()).isEqualTo(query); + assertThat(captor1.getValue().getOperationName()).isEqualTo(operationName); + assertThat(captor1.getValue().getVariables()).isSameAs(variables); + + invoke.block(); + + verify(graphQL).executeAsync(executionInputResult); + + } + + +} \ No newline at end of file diff --git a/graphql-java-spring-webmvc/build.gradle b/graphql-java-spring-webmvc/build.gradle index b207610..c977326 100644 --- a/graphql-java-spring-webmvc/build.gradle +++ b/graphql-java-spring-webmvc/build.gradle @@ -6,6 +6,7 @@ dependencies { compile "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" compile "com.graphql-java:graphql-java:$graphqlJavaVersion" + testCompile("org.assertj:assertj-core:$assertJVersion") testCompile group: 'junit', name: 'junit', version: '4.12' testCompile "org.springframework:spring-test:$springVersion" testCompile group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1' diff --git a/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/ExecutionInputCustomizer.java b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/ExecutionInputCustomizer.java new file mode 100644 index 0000000..91e0cb7 --- /dev/null +++ b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/ExecutionInputCustomizer.java @@ -0,0 +1,20 @@ +package graphql.spring.web.servlet; + +import graphql.ExecutionInput; +import graphql.PublicApi; +import org.springframework.web.context.request.WebRequest; + +import java.util.concurrent.CompletableFuture; + +/** + * Lets you customize the #ExecutionInput before the query is executed. + * You can for example set a context object or define a root value. + *

+ * This is only used if you use the default {@link GraphQLInvocation}. + */ +@PublicApi +public interface ExecutionInputCustomizer { + + CompletableFuture customizeExecutionInput(ExecutionInput executionInput, WebRequest webRequest); + +} diff --git a/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/DefaultExecutionInputCustomizer.java b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/DefaultExecutionInputCustomizer.java new file mode 100644 index 0000000..cf75683 --- /dev/null +++ b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/DefaultExecutionInputCustomizer.java @@ -0,0 +1,19 @@ +package graphql.spring.web.servlet.components; + +import graphql.ExecutionInput; +import graphql.Internal; +import graphql.spring.web.servlet.ExecutionInputCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.WebRequest; + +import java.util.concurrent.CompletableFuture; + +@Component +@Internal +public class DefaultExecutionInputCustomizer implements ExecutionInputCustomizer { + + @Override + public CompletableFuture customizeExecutionInput(ExecutionInput executionInput, WebRequest webRequest) { + return CompletableFuture.completedFuture(executionInput); + } +} diff --git a/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/DefaultGraphQLInvocation.java b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/DefaultGraphQLInvocation.java index 4a3ef15..9e5dfe7 100644 --- a/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/DefaultGraphQLInvocation.java +++ b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/DefaultGraphQLInvocation.java @@ -4,6 +4,7 @@ import graphql.ExecutionResult; import graphql.GraphQL; import graphql.Internal; +import graphql.spring.web.servlet.ExecutionInputCustomizer; import graphql.spring.web.servlet.GraphQLInvocation; import graphql.spring.web.servlet.GraphQLInvocationData; import org.springframework.beans.factory.annotation.Autowired; @@ -17,7 +18,10 @@ public class DefaultGraphQLInvocation implements GraphQLInvocation { @Autowired - private GraphQL graphQL; + GraphQL graphQL; + + @Autowired + ExecutionInputCustomizer executionInputCustomizer; @Override public CompletableFuture invoke(GraphQLInvocationData invocationData, WebRequest webRequest) { @@ -26,7 +30,8 @@ public CompletableFuture invoke(GraphQLInvocationData invocatio .operationName(invocationData.getOperationName()) .variables(invocationData.getVariables()) .build(); - return graphQL.executeAsync(executionInput); + CompletableFuture customizedExecutionInput = executionInputCustomizer.customizeExecutionInput(executionInput, webRequest); + return customizedExecutionInput.thenCompose(graphQL::executeAsync); } } diff --git a/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DefaultGraphQLInvocationTest.java b/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DefaultGraphQLInvocationTest.java new file mode 100644 index 0000000..6d2a117 --- /dev/null +++ b/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DefaultGraphQLInvocationTest.java @@ -0,0 +1,59 @@ +package graphql.spring.web.servlet.components; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.spring.web.servlet.ExecutionInputCustomizer; +import graphql.spring.web.servlet.GraphQLInvocationData; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.web.context.request.WebRequest; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DefaultGraphQLInvocationTest { + + + @Test + public void testCustomizerIsCalled() { + + String query = "query myQuery {foo}"; + String operationName = "myQuery"; + Map variables = new LinkedHashMap<>(); + + DefaultGraphQLInvocation defaultGraphQLInvocation = new DefaultGraphQLInvocation(); + ExecutionInputCustomizer executionInputCustomizer = mock(ExecutionInputCustomizer.class); + defaultGraphQLInvocation.executionInputCustomizer = executionInputCustomizer; + GraphQL graphQL = mock(GraphQL.class); + defaultGraphQLInvocation.graphQL = graphQL; + ExecutionResult executionResult = mock(ExecutionResult.class); + when(graphQL.executeAsync(any(ExecutionInput.class))).thenReturn(completedFuture(executionResult)); + + GraphQLInvocationData graphQLInvocationData = new GraphQLInvocationData(query, operationName, variables); + WebRequest webRequest = mock(WebRequest.class); + + ArgumentCaptor captor1 = ArgumentCaptor.forClass(ExecutionInput.class); + ArgumentCaptor captor2 = ArgumentCaptor.forClass(WebRequest.class); + ExecutionInput executionInputResult = mock(ExecutionInput.class); + when(executionInputCustomizer.customizeExecutionInput(captor1.capture(), captor2.capture())).thenReturn(completedFuture(executionInputResult)); + + CompletableFuture invoke = defaultGraphQLInvocation.invoke(graphQLInvocationData, webRequest); + + assertThat(captor1.getValue().getQuery()).isEqualTo(query); + assertThat(captor1.getValue().getOperationName()).isEqualTo(operationName); + assertThat(captor1.getValue().getVariables()).isSameAs(variables); + + verify(graphQL).executeAsync(executionInputResult); + + } + +} \ No newline at end of file