Updates to Interceptor contract
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
public interface GraphQLInterceptor {
|
||||
|
||||
ExecutionInput preHandle(ExecutionInput input, HttpHeaders headers);
|
||||
|
||||
ExecutionResult postHandle(ExecutionResult result);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
|
||||
/**
|
||||
* @author Andreas Marek
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class RequestInput {
|
||||
|
||||
private String query;
|
||||
|
||||
private String operationName;
|
||||
|
||||
private Map<String, Object> variables = Collections.emptyMap();
|
||||
|
||||
public RequestInput(String query, String operationName, Map<String, Object> variables) {
|
||||
this.query = query;
|
||||
this.operationName = operationName;
|
||||
this.variables = variables;
|
||||
}
|
||||
|
||||
public RequestInput() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
public void setQuery(String query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public String getOperationName() {
|
||||
return this.operationName;
|
||||
}
|
||||
|
||||
public void setOperationName(String operationName) {
|
||||
this.operationName = operationName;
|
||||
}
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
public void setVariables(Map<String, Object> variables) {
|
||||
this.variables = variables;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if (!StringUtils.hasText(getQuery())) {
|
||||
throw new ServerWebInputException("Missing query");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import graphql.ExecutionResult;
|
||||
import graphql.GraphQL;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
@@ -38,26 +37,29 @@ public class WebFluxGraphQLHandler implements HandlerFunction<ServerResponse> {
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> handle(ServerRequest request) {
|
||||
return request.bodyToMono(RequestInput.class)
|
||||
.flatMap(requestInput -> {
|
||||
requestInput.validate();
|
||||
return request.bodyToMono(WebInput.MAP_PARAMETERIZED_TYPE_REF)
|
||||
.flatMap(body -> {
|
||||
WebInput webInput = new WebInput(
|
||||
request.uri(), request.headers().asHttpHeaders(), body);
|
||||
|
||||
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
|
||||
.query(requestInput.getQuery())
|
||||
.operationName(requestInput.getOperationName())
|
||||
.variables(requestInput.getVariables())
|
||||
.query(webInput.getQuery())
|
||||
.operationName(webInput.getOperationName())
|
||||
.variables(webInput.getVariables())
|
||||
.build();
|
||||
|
||||
// Invoke GraphQLInterceptor's preHandle here
|
||||
return customizeExecutionInput(executionInput, request.headers().asHttpHeaders());
|
||||
return extendInput(executionInput, webInput);
|
||||
})
|
||||
.flatMap(input -> {
|
||||
// Invoke GraphQLInterceptor's postHandle here
|
||||
return execute(input);
|
||||
.flatMap(executionInput -> {
|
||||
// Invoke handleResult here
|
||||
return execute(executionInput);
|
||||
})
|
||||
.flatMap(result -> ServerResponse.ok().bodyValue(result.toSpecification()));
|
||||
}
|
||||
|
||||
protected Mono<ExecutionInput> customizeExecutionInput(ExecutionInput input, HttpHeaders headers) {
|
||||
return Mono.just(input);
|
||||
protected Mono<ExecutionInput> extendInput(ExecutionInput executionInput, WebInput webInput) {
|
||||
return Mono.just(executionInput);
|
||||
}
|
||||
|
||||
protected Mono<ExecutionResult> execute(ExecutionInput input) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* Represents the input to a GraphQL HTTP endpoint including URI, headers, and
|
||||
* the query, operationName, and variables from the request body.
|
||||
*/
|
||||
public class WebInput {
|
||||
|
||||
static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
|
||||
|
||||
private final UriComponents uri;
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private final String query;
|
||||
|
||||
@Nullable
|
||||
private final String operationName;
|
||||
|
||||
private final Map<String, Object> variables;
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
WebInput(URI uri, HttpHeaders headers, Map<String, Object> body) {
|
||||
this.uri = UriComponentsBuilder.fromUri(uri).build(true);
|
||||
this.headers = headers;
|
||||
this.query = getAndValidateQuery(body);
|
||||
this.operationName = (String) body.get("operationName");
|
||||
this.variables = (Map<String, Object>) body.getOrDefault("variables", Collections.emptyMap());
|
||||
}
|
||||
|
||||
private static String getAndValidateQuery(Map<String, Object> body) {
|
||||
String query = (String) body.get("query");
|
||||
if (!StringUtils.hasText(query)) {
|
||||
throw new ServerWebInputException("Query is required");
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
public UriComponents uri() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getOperationName() {
|
||||
return this.operationName;
|
||||
}
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Allows interception of GraphQL over HTTP requests with possible customization
|
||||
* of the input and the result of query execution.
|
||||
*/
|
||||
public interface WebInterceptor {
|
||||
|
||||
/**
|
||||
* Intercept a GraphQL over HTTP request before the query is executed.
|
||||
*
|
||||
* @param executionInput the input to use, initialized from the {@code WebInput}
|
||||
* @param webInput the input from the HTTP request
|
||||
* @return the same instance or a new one via {@link ExecutionInput#transform(Consumer)}
|
||||
*/
|
||||
default Mono<ExecutionInput> preHandle(ExecutionInput executionInput, WebInput webInput) {
|
||||
return Mono.just(executionInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept a GraphQL over HTTP request after the query is executed.
|
||||
*
|
||||
* @param webOutput the execution result
|
||||
* @return the same instance or a new one via {@link WebOutput#transform(Consumer)}
|
||||
*/
|
||||
default Mono<WebOutput> postHandle(WebOutput webOutput) {
|
||||
return Mono.just(webOutput);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,18 +17,15 @@ package org.springframework.graphql;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQL;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.server.ServerErrorException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.servlet.function.HandlerFunction;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
@@ -53,47 +50,38 @@ public class WebMvcGraphQLHandler implements HandlerFunction<ServerResponse> {
|
||||
* e.g. {@link HttpMediaTypeNotSupportedException}.
|
||||
*/
|
||||
public ServerResponse handle(ServerRequest request) throws ServletException {
|
||||
RequestInput requestInput;
|
||||
WebInput webInput = createWebInput(request);
|
||||
|
||||
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
|
||||
.query(webInput.getQuery())
|
||||
.operationName(webInput.getOperationName())
|
||||
.variables(webInput.getVariables())
|
||||
.build();
|
||||
|
||||
Mono<Map<String, Object>> body = extendInput(executionInput, webInput)
|
||||
.flatMap(this::execute)
|
||||
.map(ExecutionResult::toSpecification);
|
||||
|
||||
return ServerResponse.ok().body(body);
|
||||
}
|
||||
|
||||
private static WebInput createWebInput(ServerRequest request) throws ServletException {
|
||||
Map<String, Object> body;
|
||||
try {
|
||||
requestInput = request.body(RequestInput.class);
|
||||
requestInput.validate();
|
||||
body = request.body(WebInput.MAP_PARAMETERIZED_TYPE_REF);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ServerWebInputException("I/O error while reading request body", null, ex);
|
||||
}
|
||||
|
||||
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
|
||||
.query(requestInput.getQuery())
|
||||
.operationName(requestInput.getOperationName())
|
||||
.variables(requestInput.getVariables())
|
||||
.build();
|
||||
|
||||
// Invoke GraphQLInterceptor's preHandle here
|
||||
|
||||
CompletableFuture<Map<String, Object>> future =
|
||||
customizeExecutionInput(executionInput, request.headers().asHttpHeaders())
|
||||
.thenCompose(this::execute)
|
||||
.thenApply(ExecutionResult::toSpecification);
|
||||
|
||||
// Invoke GraphQLInterceptor's postHandle here
|
||||
|
||||
return ServerResponse.ok().body(future.isDone() ? getResult(future) : future);
|
||||
return new WebInput(request.uri(), request.headers().asHttpHeaders(), body);
|
||||
}
|
||||
|
||||
protected CompletableFuture<ExecutionInput> customizeExecutionInput(ExecutionInput input, HttpHeaders headers) {
|
||||
return CompletableFuture.completedFuture(input);
|
||||
protected Mono<ExecutionInput> extendInput(ExecutionInput executionInput, WebInput webInput) {
|
||||
return Mono.just(executionInput);
|
||||
}
|
||||
|
||||
protected CompletableFuture<ExecutionResult> execute(ExecutionInput input) {
|
||||
return graphQL.executeAsync(input);
|
||||
protected Mono<ExecutionResult> execute(ExecutionInput input) {
|
||||
return Mono.fromFuture(this.graphQL.executeAsync(input));
|
||||
}
|
||||
|
||||
private Map<String, Object> getResult(CompletableFuture<Map<String, Object>> future) {
|
||||
try {
|
||||
return future.get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException ex) {
|
||||
throw new ServerErrorException("Failed to get result", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2020-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.ExecutionResultImpl;
|
||||
import graphql.GraphQLError;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
|
||||
public class WebOutput implements ExecutionResult {
|
||||
|
||||
private final ExecutionResult executionResult;
|
||||
|
||||
|
||||
WebOutput(ExecutionResult executionResult) {
|
||||
this.executionResult = executionResult;
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <T> T getData() {
|
||||
return this.executionResult.getData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDataPresent() {
|
||||
return this.executionResult.isDataPresent();
|
||||
}
|
||||
|
||||
public List<GraphQLError> getErrors() {
|
||||
return this.executionResult.getErrors();
|
||||
}
|
||||
|
||||
public Map<Object, Object> getExtensions() {
|
||||
return this.executionResult.getExtensions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toSpecification() {
|
||||
return this.executionResult.toSpecification();
|
||||
}
|
||||
|
||||
public WebOutput transform(Consumer<Builder> consumer) {
|
||||
Builder builder = new Builder(this);
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
||||
public static class Builder {
|
||||
|
||||
@Nullable
|
||||
private Object data;
|
||||
|
||||
private List<GraphQLError> errors;
|
||||
|
||||
private Map<Object, Object> extensions;
|
||||
|
||||
public Builder(WebOutput output) {
|
||||
this.data = output.getData();
|
||||
this.errors = output.getErrors();
|
||||
this.extensions = output.getExtensions();
|
||||
}
|
||||
|
||||
public Builder data(Object data) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder errors(List<GraphQLError> errors) {
|
||||
this.errors = errors;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder extensions(Map<Object, Object> extensions) {
|
||||
this.extensions = extensions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public WebOutput build() {
|
||||
return new WebOutput(new ExecutionResultImpl(this.data, this.errors, this.extensions));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user