Merge modules
This commit merges the common, webmvc and webflux modules into a single web module for supporting GraphQL with all Spring web frameworks. We're working here with optional dependencies to build the support for Spring MVC on one side and Spring WebFlux on the other. Also, this commit updates the starter modules to adopt the official naming convention. For now, starter modules will host both the configuration infrastructure and the transitive dependencies to activate support.
This commit is contained in:
16
spring-graphql-web/build.gradle
Normal file
16
spring-graphql-web/build.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
description = "GraphQL Java support with Spring Web frameworks"
|
||||
|
||||
dependencies {
|
||||
api "com.graphql-java:graphql-java"
|
||||
|
||||
compileOnly "org.springframework:spring-context"
|
||||
compileOnly "org.springframework:spring-webflux"
|
||||
|
||||
compileOnly 'org.springframework:spring-webmvc'
|
||||
compileOnly 'javax.servlet:javax.servlet-api'
|
||||
|
||||
testImplementation "com.fasterxml.jackson.core:jackson-databind"
|
||||
testImplementation "org.springframework:spring-webflux"
|
||||
testImplementation 'org.springframework:spring-webmvc'
|
||||
testImplementation 'javax.servlet:javax.servlet-api'
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.springframework.graphql;
|
||||
|
||||
public class DefaultGraphQLInterceptor implements GraphQLInterceptor {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.springframework.graphql;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQL;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
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<GraphQLHttpResponse> graphqlPOST(GraphQLHttpRequest graphQLHttpRequest) {
|
||||
String query = graphQLHttpRequest.getQuery();
|
||||
ExecutionInput input = ExecutionInput.newExecutionInput()
|
||||
.query(query)
|
||||
.operationName(graphQLHttpRequest.getOperationName())
|
||||
.variables(graphQLHttpRequest.getVariables())
|
||||
.build();
|
||||
MultiValueMap<String, String> requestParams = graphQLHttpRequest.getRequestParams();
|
||||
Mono<ExecutionInput> executionInput = interceptor.preHandle(input,
|
||||
graphQLHttpRequest.getHttpHeaders(),
|
||||
requestParams);
|
||||
return executionInput
|
||||
.flatMap(this::execute)
|
||||
.flatMap(result -> interceptor.postHandle(result, graphQLHttpRequest.getHttpHeaders(), requestParams))
|
||||
.flatMap(result -> toResponseBody(result, graphQLHttpRequest));
|
||||
}
|
||||
|
||||
private Mono<GraphQLHttpResponse> toResponseBody(ExecutionResult executionResult, GraphQLHttpRequest graphQLHttpRequest) {
|
||||
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");
|
||||
GraphQLHttpResponse responseBody = new GraphQLHttpResponse(data,
|
||||
errors,
|
||||
extensions,
|
||||
new HttpHeaders());
|
||||
Mono<GraphQLHttpResponse> graphQLResponseBodyMono = interceptor.customizeGraphQLHttpResponse(responseBody, executionResult, graphQLHttpRequest);
|
||||
return graphQLResponseBodyMono;
|
||||
}
|
||||
|
||||
|
||||
protected Mono<ExecutionResult> execute(ExecutionInput input) {
|
||||
return Mono.fromCompletionStage(graphQL.executeAsync(input));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.springframework.graphql;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class GraphQLHttpRequest {
|
||||
private final String query;
|
||||
private final String operationName;
|
||||
private final Map<String, Object> variables;
|
||||
private final HttpHeaders httpHeaders;
|
||||
private final MultiValueMap<String, String> requestParams;
|
||||
|
||||
public GraphQLHttpRequest(String query,
|
||||
String operationName,
|
||||
Map<String, Object> variables,
|
||||
HttpHeaders httpHeaders,
|
||||
MultiValueMap<String, String> requestParams) {
|
||||
this.query = query;
|
||||
this.operationName = operationName;
|
||||
this.variables = variables;
|
||||
this.httpHeaders = httpHeaders;
|
||||
this.requestParams = requestParams;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public String getOperationName() {
|
||||
return operationName;
|
||||
}
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
return variables;
|
||||
}
|
||||
|
||||
public HttpHeaders getHttpHeaders() {
|
||||
return httpHeaders;
|
||||
}
|
||||
|
||||
public MultiValueMap<String, String> getRequestParams() {
|
||||
return requestParams;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.springframework.graphql;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class GraphQLHttpResponse {
|
||||
|
||||
private final Object data;
|
||||
private final List<Map<String, Object>> errors;
|
||||
private final Map<String, Object> extensions;
|
||||
private final HttpHeaders httpHeaders;
|
||||
|
||||
public GraphQLHttpResponse(Object data,
|
||||
List<Map<String, Object>> errors,
|
||||
Map<String, Object> extensions,
|
||||
HttpHeaders httpHeaders) {
|
||||
this.data = data;
|
||||
this.errors = errors;
|
||||
this.extensions = extensions;
|
||||
this.httpHeaders = httpHeaders;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
public Map<String, Object> getExtensions() {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
public HttpHeaders getHttpHeaders() {
|
||||
return httpHeaders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.springframework.graphql;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface GraphQLInterceptor {
|
||||
|
||||
default Mono<ExecutionInput> preHandle(ExecutionInput input,
|
||||
HttpHeaders headers,
|
||||
MultiValueMap<String, String> requestParams) {
|
||||
return Mono.just(input);
|
||||
}
|
||||
|
||||
default Mono<ExecutionResult> postHandle(ExecutionResult result,
|
||||
HttpHeaders httpHeaders,
|
||||
MultiValueMap<String, String> requestParams) {
|
||||
return Mono.just(result);
|
||||
}
|
||||
|
||||
default Mono<GraphQLHttpResponse> customizeGraphQLHttpResponse(GraphQLHttpResponse graphQLHttpResponse,
|
||||
ExecutionResult executionResult,
|
||||
GraphQLHttpRequest graphQLHttpRequest) {
|
||||
|
||||
return Mono.just(graphQLHttpResponse);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,86 @@
|
||||
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.DefaultGraphQLInterceptor;
|
||||
import org.springframework.graphql.GraphQLHandler;
|
||||
import org.springframework.graphql.GraphQLHttpRequest;
|
||||
import org.springframework.graphql.GraphQLInterceptor;
|
||||
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;
|
||||
|
||||
@Configuration
|
||||
public class GraphQLController {
|
||||
|
||||
@Autowired
|
||||
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() {
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.POST("/graphql", this::graphqlPOST)
|
||||
.build();
|
||||
return route;
|
||||
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> graphqlPOST(ServerRequest serverRequest) {
|
||||
Mono<GraphQLReactiveRequestBody> bodyMono = serverRequest.bodyToMono(GraphQLReactiveRequestBody.class);
|
||||
return bodyMono.flatMap(body -> {
|
||||
String query = body.getQuery();
|
||||
if (query == null) {
|
||||
query = "";
|
||||
}
|
||||
Map<String, Object> variables = body.getVariables();
|
||||
if (variables == null) {
|
||||
variables = Collections.emptyMap();
|
||||
}
|
||||
GraphQLHttpRequest graphQLHttpRequest = new GraphQLHttpRequest(
|
||||
query,
|
||||
body.getOperationName(),
|
||||
variables,
|
||||
serverRequest.headers().asHttpHeaders(),
|
||||
serverRequest.queryParams());
|
||||
return graphQLHandler.graphqlPOST(graphQLHttpRequest);
|
||||
}).flatMap(graphQLHttpResponse -> {
|
||||
//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", graphQLHttpResponse.getData());
|
||||
if (graphQLHttpResponse.getErrors() != null) {
|
||||
responseBodyRaw.put("errors", graphQLHttpResponse.getErrors());
|
||||
}
|
||||
if (graphQLHttpResponse.getExtensions() != null) {
|
||||
responseBodyRaw.put("extensions", graphQLHttpResponse.getExtensions());
|
||||
}
|
||||
return ServerResponse.ok().headers(httpHeaders -> {
|
||||
httpHeaders.addAll(graphQLHttpResponse.getHttpHeaders());
|
||||
}).bodyValue(responseBodyRaw);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.springframework.graphql.reactive.components;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class GraphQLReactiveRequestBody {
|
||||
private String query;
|
||||
private String operationName;
|
||||
private Map<String, Object> 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<String, Object> getVariables() {
|
||||
return variables;
|
||||
}
|
||||
|
||||
public void setVariables(Map<String, Object> variables) {
|
||||
this.variables = variables;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.reactive.components;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.springframework.graphql.servlet.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.DefaultGraphQLInterceptor;
|
||||
import org.springframework.graphql.GraphQLHandler;
|
||||
import org.springframework.graphql.GraphQLHttpRequest;
|
||||
import org.springframework.graphql.GraphQLInterceptor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.function.RequestPredicates;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.web.servlet.function.RouterFunctions.route;
|
||||
|
||||
@Configuration
|
||||
public class GraphQLController {
|
||||
|
||||
@Autowired
|
||||
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() {
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.POST("/graphql", RequestPredicates.contentType(MediaType.APPLICATION_JSON)
|
||||
.or(RequestPredicates.contentType(MediaType.APPLICATION_JSON_UTF8)), this::graphqlPOST)
|
||||
.build();
|
||||
return route;
|
||||
|
||||
}
|
||||
|
||||
private ServerResponse graphqlPOST(ServerRequest serverRequest) {
|
||||
GraphQLServletRequestBody body = null;
|
||||
try {
|
||||
body = serverRequest.body(GraphQLServletRequestBody.class);
|
||||
} catch (ServletException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
String query = body.getQuery();
|
||||
if (query == null) {
|
||||
query = "";
|
||||
}
|
||||
Map<String, Object> variables = body.getVariables();
|
||||
if (variables == null) {
|
||||
variables = Collections.emptyMap();
|
||||
}
|
||||
|
||||
GraphQLHttpRequest graphQLHttpRequest = new GraphQLHttpRequest(
|
||||
query,
|
||||
body.getOperationName(),
|
||||
variables,
|
||||
serverRequest.headers().asHttpHeaders(),
|
||||
serverRequest.params());
|
||||
|
||||
Mono<Map<String, Object>> responseRawMono = graphQLHandler.graphqlPOST(graphQLHttpRequest)
|
||||
.map(graphQLHttpResponse -> {
|
||||
//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", graphQLHttpResponse.getData());
|
||||
if (graphQLHttpResponse.getErrors() != null) {
|
||||
responseBodyRaw.put("errors", graphQLHttpResponse.getErrors());
|
||||
}
|
||||
if (graphQLHttpResponse.getExtensions() != null) {
|
||||
responseBodyRaw.put("extensions", graphQLHttpResponse.getExtensions());
|
||||
}
|
||||
return responseBodyRaw;
|
||||
});
|
||||
//TODO: how to add http headers for the response here? It is part of responseRawMono but
|
||||
// ServerResponse don't accept a CompletableFuture or Mono as headers.
|
||||
return ServerResponse.ok().body(responseRawMono);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.springframework.graphql.servlet.components;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class GraphQLServletRequestBody {
|
||||
private String query;
|
||||
private String operationName;
|
||||
private Map<String, Object> 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<String, Object> getVariables() {
|
||||
return variables;
|
||||
}
|
||||
|
||||
public void setVariables(Map<String, Object> variables) {
|
||||
this.variables = variables;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.servlet.components;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
Reference in New Issue
Block a user