Updates to Interceptor contract

This commit is contained in:
Rossen Stoyanchev
2020-09-18 11:37:40 +01:00
parent 0e9afb392a
commit e8a8334009
7 changed files with 285 additions and 153 deletions

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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