Skip to content

Commit

Permalink
introduce common GraphQLHandler which doesn't depend on either webflu…
Browse files Browse the repository at this point in the history
…x or webmvc specifics.

Refactor webflux to use the new Handler
  • Loading branch information
andimarek committed Sep 13, 2020
1 parent 7edc2d9 commit a37aa74
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 60 deletions.
1 change: 1 addition & 0 deletions settings.gradle
Expand Up @@ -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'

15 changes: 15 additions & 0 deletions spring-graphql-common/build.gradle
@@ -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.+"
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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);

}
@@ -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() {
Expand Down
@@ -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;
}
}
1 change: 1 addition & 0 deletions spring-graphql-webflux/build.gradle
Expand Up @@ -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')
Expand Down
@@ -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;
Expand All @@ -18,7 +25,18 @@
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() {
Expand All @@ -30,15 +48,31 @@ public RouterFunction<ServerResponse> routerFunction() {
}

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);
});
}

Expand Down
Expand Up @@ -2,7 +2,7 @@

import java.util.Map;

public class GraphQLRequestBody {
public class GraphQLReactiveRequestBody {
private String query;
private String operationName;
private Map<String, Object> variables;
Expand Down

This file was deleted.

0 comments on commit a37aa74

Please sign in to comment.