introduce common GraphQLHandler which doesn't depend on either webflux or webmvc specifics.

Refactor webflux to use the new Handler
This commit is contained in:
Andreas Marek
2020-09-14 07:13:21 +10:00
parent 7edc2d9ce6
commit a37aa740a9
11 changed files with 187 additions and 60 deletions

View File

@@ -3,4 +3,5 @@ include 'spring-graphql-boot-starter-webmvc'
include 'spring-graphql-boot-starter-webflux'
include 'spring-graphql-webmvc'
include 'spring-graphql-webflux'
include 'spring-graphql-common'

View File

@@ -0,0 +1,15 @@
description = "GraphQL Java Spring Common"
apply plugin: 'java-library'
dependencies {
implementation "org.springframework:spring-web:$springVersion"
implementation 'io.projectreactor:reactor-core:3.3.8.RELEASE'
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.+"
}

View File

@@ -0,0 +1,24 @@
package org.springframework.graphql;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;
public class DefaultGraphQLInterceptor implements GraphQLInterceptor {
@Override
public Mono<ExecutionInput> preHandle(ExecutionInput input, HttpHeaders headers) {
return Mono.just(input);
}
@Override
public Mono<ExecutionResult> postHandle(ExecutionResult result, HttpHeaders httpHeaders) {
return Mono.just(result);
}
@Override
public Mono<GraphQLResponseBody> customizeResponseBody(GraphQLResponseBody graphQLResponseBody, ExecutionResult executionResult, HttpHeaders httpHeader) {
return Mono.just(graphQLResponseBody);
}
}

View File

@@ -0,0 +1,55 @@
package org.springframework.graphql;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
public class GraphQLHandler {
private GraphQL graphQL;
private GraphQLInterceptor interceptor;
public GraphQLHandler(GraphQL graphQL, GraphQLInterceptor interceptor) {
this.graphQL = graphQL;
this.interceptor = interceptor;
}
public Mono<GraphQLResponseBody> graphqlPOST(GraphQLRequestBody body, HttpHeaders httpHeaders) {
String query = body.getQuery();
if (query == null) {
query = "";
}
ExecutionInput input = ExecutionInput.newExecutionInput()
.query(query)
.operationName(body.getOperationName())
.variables(body.getVariables())
.build();
Mono<ExecutionInput> executionInput = interceptor.preHandle(input, httpHeaders);
return executionInput
.flatMap(this::execute)
.flatMap(result -> interceptor.postHandle(result, httpHeaders))
.flatMap(result -> toResponseBody(result, httpHeaders));
}
private Mono<GraphQLResponseBody> toResponseBody(ExecutionResult executionResult, HttpHeaders httpHeaders) {
Map<String, Object> responseBodyRaw = executionResult.toSpecification();
Object data = responseBodyRaw.get("data");
List<Map<String, Object>> errors = (List<Map<String, Object>>) responseBodyRaw.get("errors");
Map<String, Object> extensions = (Map<String, Object>) responseBodyRaw.get("extensions");
GraphQLResponseBody responseBody = new GraphQLResponseBody(data,
errors,
extensions);
Mono<GraphQLResponseBody> graphQLResponseBodyMono = interceptor.customizeResponseBody(responseBody, executionResult, httpHeaders);
return graphQLResponseBodyMono;
}
protected Mono<ExecutionResult> execute(ExecutionInput input) {
return Mono.fromCompletionStage(graphQL.executeAsync(input));
}
}

View File

@@ -0,0 +1,16 @@
package org.springframework.graphql;
import graphql.ExecutionInput;
import graphql.ExecutionResult;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;
public interface GraphQLInterceptor {
Mono<ExecutionInput> preHandle(ExecutionInput input, HttpHeaders headers);
Mono<ExecutionResult> postHandle(ExecutionResult result, HttpHeaders httpHeaders);
Mono<GraphQLResponseBody> customizeResponseBody(GraphQLResponseBody graphQLResponseBody, ExecutionResult executionResult, HttpHeaders httpHeader);
}

View File

@@ -1,20 +1,16 @@
package org.springframework.graphql.reactive;
package org.springframework.graphql;
import graphql.Assert;
import java.util.Collections;
import java.util.Map;
public class GraphQLInvocationData {
public class GraphQLRequestBody {
private final String query;
private final String operationName;
private final Map<String, Object> variables;
public GraphQLInvocationData(String query, String operationName, Map<String, Object> variables) {
this.query = Assert.assertNotNull(query, () -> "query must be provided");
public GraphQLRequestBody(String query, String operationName, Map<String, Object> variables) {
this.query = query;
this.operationName = operationName;
this.variables = variables != null ? variables : Collections.emptyMap();
this.variables = variables;
}
public String getQuery() {

View File

@@ -0,0 +1,29 @@
package org.springframework.graphql;
import java.util.List;
import java.util.Map;
public class GraphQLResponseBody {
private final Object data;
private final List<Map<String, Object>> errors;
private final Map<String, Object> extensions;
public GraphQLResponseBody(Object data, List<Map<String, Object>> errors, Map<String, Object> extensions) {
this.data = data;
this.errors = errors;
this.extensions = extensions;
}
public Object getData() {
return data;
}
public List<Map<String, Object>> getErrors() {
return errors;
}
public Map<String, Object> getExtensions() {
return extensions;
}
}

View File

@@ -7,6 +7,7 @@ dependencies {
implementation "org.springframework:spring-context:$springVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
api "com.graphql-java:graphql-java:$graphqlJavaVersion"
implementation project(':spring-graphql-common')
testImplementation("org.assertj:assertj-core:$assertJVersion")
testImplementation('org.junit.jupiter:junit-jupiter:5.6.2')

View File

@@ -1,15 +1,22 @@
package org.springframework.graphql.reactive.components;
import graphql.GraphQL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.reactive.GraphQLInvocationData;
import org.springframework.graphql.DefaultGraphQLInterceptor;
import org.springframework.graphql.GraphQLHandler;
import org.springframework.graphql.GraphQLInterceptor;
import org.springframework.graphql.GraphQLRequestBody;
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 javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@@ -18,7 +25,18 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
public class GraphQLController {
@Autowired
GraphQLRequestHandler graphQLRequestHandler;
GraphQL graphQL;
GraphQLHandler graphQLHandler;
@Autowired(required = false)
GraphQLInterceptor graphQLInterceptor;
@PostConstruct
public void init() {
GraphQLInterceptor interceptor = graphQLInterceptor == null ? new DefaultGraphQLInterceptor() : graphQLInterceptor;
this.graphQLHandler = new GraphQLHandler(graphQL, interceptor);
}
@Bean
public RouterFunction<ServerResponse> routerFunction() {
@@ -30,15 +48,31 @@ public class GraphQLController {
}
private Mono<ServerResponse> graphqlPOST(ServerRequest serverRequest) {
Mono<GraphQLRequestBody> bodyMono = serverRequest.bodyToMono(GraphQLRequestBody.class);
Mono<GraphQLReactiveRequestBody> bodyMono = serverRequest.bodyToMono(GraphQLReactiveRequestBody.class);
return bodyMono.flatMap(body -> {
String query = body.getQuery();
if (query == null) {
query = "";
}
GraphQLInvocationData invocationData = new GraphQLInvocationData(query, body.getOperationName(), body.getVariables());
Mono<Map> resultBodyMono = graphQLRequestHandler.invoke(invocationData, serverRequest.headers());
return resultBodyMono.flatMap(resultBody -> ServerResponse.ok().bodyValue(resultBody));
Map<String, Object> variables = body.getVariables();
if (variables == null) {
variables = Collections.emptyMap();
}
GraphQLRequestBody graphQLRequestBody = new GraphQLRequestBody(query, body.getOperationName(), variables);
return graphQLHandler.graphqlPOST(graphQLRequestBody, serverRequest.headers().asHttpHeaders());
}).flatMap(graphQLResponseBody -> {
//TODO: this should be handled better:
// we don't want to serialize `null` values for `errors` and `extensions`
// this is why we convert it to a Map here
Map<String, Object> responseBodyRaw = new LinkedHashMap<>();
responseBodyRaw.put("data", graphQLResponseBody.getData());
if (graphQLResponseBody.getErrors() != null) {
responseBodyRaw.put("errors", graphQLResponseBody.getErrors());
}
if (graphQLResponseBody.getExtensions() != null) {
responseBodyRaw.put("extensions", graphQLResponseBody.getExtensions());
}
return ServerResponse.ok().bodyValue(responseBodyRaw);
});
}

View File

@@ -2,7 +2,7 @@ package org.springframework.graphql.reactive.components;
import java.util.Map;
public class GraphQLRequestBody {
public class GraphQLReactiveRequestBody {
private String query;
private String operationName;
private Map<String, Object> variables;

View File

@@ -1,44 +0,0 @@
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<Map> 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<ExecutionInput> customizedExecutionInput = customizeExecutionInput(executionInput, headers);
CompletableFuture<ExecutionResult> executionResultCompletableFuture = customizedExecutionInput.thenCompose(graphQL::executeAsync);
return handleExecutionResult(executionResultCompletableFuture);
}
protected CompletableFuture<ExecutionInput> customizeExecutionInput(ExecutionInput executionInput,
ServerRequest.Headers headers) {
return CompletableFuture.completedFuture(executionInput);
}
protected Mono<Map> handleExecutionResult(CompletableFuture<ExecutionResult> executionResultCF) {
return Mono.fromCompletionStage(executionResultCF).map(ExecutionResult::toSpecification);
}
}