Set ExecutionId on web requests
Prior to this commit, the `WebInput` would hold an `id` attribute that correlate the request and response messages on a multiplexed connection. This commit moves this concept directly to the `RequestInput` so that it can be used as an `ExecutionId` for all types of transports, if availble. This id was already set on the `WebInput` in the WebFlux case with the id coming from the exchange, but it also creates an id and set it for the MVC case. This id is then propagated to GraphQL's `ExecutionInput`. See gh-183
This commit is contained in:
@@ -29,6 +29,7 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link WebGraphQlTester}.
|
||||
@@ -161,7 +162,8 @@ class DefaultWebGraphQlTester implements WebGraphQlTester {
|
||||
|
||||
private WebInput createWebInput() {
|
||||
RequestInput input = createRequestInput();
|
||||
return new WebInput(DEFAULT_URL, this.headers, input.toMap(), input.getLocale(), null);
|
||||
return new WebInput(DEFAULT_URL, this.headers, input.toMap(), input.getLocale(),
|
||||
(input.getId() != null) ? input.getId() : ObjectUtils.getIdentityHexString(input));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class GraphQlTesterRequestSpecSupport {
|
||||
}
|
||||
|
||||
protected RequestInput createRequestInput() {
|
||||
return new RequestInput(this.query, this.operationName, this.variables, this.locale);
|
||||
return new RequestInput(this.query, this.operationName, this.variables, this.locale, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -227,7 +228,7 @@ public class WebGraphQlTesterTests {
|
||||
|
||||
String content = request.getBody().readUtf8();
|
||||
Map<String, Object> map = new ObjectMapper().readValue(content, new TypeReference<Map<String, Object>>() {});
|
||||
WebInput webInput = new WebInput(request.getRequestUrl().uri(), headers, map, null, null);
|
||||
WebInput webInput = new WebInput(request.getRequestUrl().uri(), headers, map, null, ObjectUtils.getIdentityHexString(request));
|
||||
|
||||
consumer.accept(webInput);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import graphql.ExecutionInput;
|
||||
import graphql.execution.ExecutionId;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -46,6 +47,9 @@ public class RequestInput {
|
||||
@Nullable
|
||||
private final String operationName;
|
||||
|
||||
@Nullable
|
||||
private final String id;
|
||||
|
||||
private final Map<String, Object> variables;
|
||||
|
||||
@Nullable
|
||||
@@ -60,16 +64,18 @@ public class RequestInput {
|
||||
* @param operationName an optional, explicit name assigned to the query
|
||||
* @param variables variables by which the query is parameterized
|
||||
* @param locale the locale associated with the request, if any
|
||||
* @param id an optional request id, to be used as the execution id
|
||||
*/
|
||||
public RequestInput(
|
||||
String query, @Nullable String operationName, @Nullable Map<String, Object> variables,
|
||||
@Nullable Locale locale) {
|
||||
@Nullable Locale locale, @Nullable String id) {
|
||||
|
||||
Assert.notNull(query, "'query' is required");
|
||||
this.query = query;
|
||||
this.operationName = operationName;
|
||||
this.variables = ((variables != null) ? variables : Collections.emptyMap());
|
||||
this.locale = locale;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +84,15 @@ public class RequestInput {
|
||||
return (T) body.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the explicitly assigned request id.
|
||||
* @return the request id or {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query, mutation, or subscription for the request.
|
||||
* @return the query, a non-empty string.
|
||||
@@ -137,6 +152,7 @@ public class RequestInput {
|
||||
.operationName(this.operationName)
|
||||
.variables(this.variables)
|
||||
.locale(this.locale)
|
||||
.executionId((this.id != null) ? ExecutionId.from(this.id) : null)
|
||||
.build();
|
||||
|
||||
for (BiFunction<ExecutionInput, ExecutionInput.Builder, ExecutionInput> configurer : this.executionInputConfigurers) {
|
||||
|
||||
@@ -45,8 +45,6 @@ public class WebInput extends RequestInput {
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* Create an instance.
|
||||
* @param uri the URL for the HTTP request or WebSocket handshake
|
||||
@@ -61,12 +59,11 @@ public class WebInput extends RequestInput {
|
||||
URI uri, HttpHeaders headers, Map<String, Object> body,
|
||||
@Nullable Locale locale, @Nullable String id) {
|
||||
|
||||
super(getKey("query", body), getKey("operationName", body), getKey("variables", body), locale);
|
||||
super(getKey("query", body), getKey("operationName", body), getKey("variables", body), locale, id);
|
||||
Assert.notNull(uri, "URI is required'");
|
||||
Assert.notNull(headers, "HttpHeaders is required'");
|
||||
this.uri = UriComponentsBuilder.fromUri(uri).build(true);
|
||||
this.headers = headers;
|
||||
this.id = (id != null) ? id : ObjectUtils.identityToString(this);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -97,8 +94,9 @@ public class WebInput extends RequestInput {
|
||||
* request and response messages on a multiplexed connection.
|
||||
* @see <a href="https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md">GraphQL over WebSocket Protocol</a>
|
||||
*/
|
||||
@Override
|
||||
public String getId() {
|
||||
return this.id;
|
||||
return (super.getId() != null) ? super.getId() : ObjectUtils.getIdentityHexString(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.WebInput;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
@@ -72,7 +73,7 @@ public class GraphQlHttpHandler {
|
||||
|
||||
WebInput input = new WebInput(
|
||||
request.uri(), request.headers().asHttpHeaders(), readBody(request),
|
||||
LocaleContextHolder.getLocale(), null);
|
||||
LocaleContextHolder.getLocale(), ObjectUtils.getIdentityHexString(request));
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing: " + input);
|
||||
|
||||
@@ -71,7 +71,7 @@ public class BatchMappingInvocationTests extends BatchMappingTestSupport {
|
||||
"}";
|
||||
|
||||
Mono<ExecutionResult> resultMono = createGraphQlService(controller)
|
||||
.execute(new RequestInput(query, null, null, null));
|
||||
.execute(new RequestInput(query, null, null, null, null));
|
||||
|
||||
List<Course> actualCourses = GraphQlResponse.from(resultMono).toList("courses", Course.class);
|
||||
List<Course> courses = Course.allCourses();
|
||||
@@ -104,7 +104,7 @@ public class BatchMappingInvocationTests extends BatchMappingTestSupport {
|
||||
"}";
|
||||
|
||||
Mono<ExecutionResult> resultMono = createGraphQlService(controller)
|
||||
.execute(new RequestInput(query, null, null, null));
|
||||
.execute(new RequestInput(query, null, null, null, null));
|
||||
|
||||
List<Course> actualCourses = GraphQlResponse.from(resultMono).toList("courses", Course.class);
|
||||
List<Course> courses = Course.allCourses();
|
||||
|
||||
@@ -95,7 +95,7 @@ public class BatchMappingPrincipalMethodArgumentResolverTests extends BatchMappi
|
||||
Mono<ExecutionResult> resultMono = Mono.delay(Duration.ofMillis(10))
|
||||
.flatMap(aLong -> {
|
||||
String query = "{ courses { id instructor { id } } }";
|
||||
return createGraphQlService(controller).execute(new RequestInput(query, null, null, null));
|
||||
return createGraphQlService(controller).execute(new RequestInput(query, null, null, null, null));
|
||||
})
|
||||
.contextWrite(contextWriter);
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public class SchemaMappingInvocationTests {
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
Mono<ExecutionResult> resultMono = graphQlService().execute(new RequestInput(query, null, null, null));
|
||||
Mono<ExecutionResult> resultMono = graphQlService().execute(new RequestInput(query, null, null, null, null));
|
||||
|
||||
Book book = GraphQlResponse.from(resultMono).toEntity("bookById", Book.class);
|
||||
assertThat(book.getId()).isEqualTo(1);
|
||||
@@ -88,7 +88,7 @@ public class SchemaMappingInvocationTests {
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
Mono<ExecutionResult> resultMono = graphQlService().execute(new RequestInput(query, null, null, null));
|
||||
Mono<ExecutionResult> resultMono = graphQlService().execute(new RequestInput(query, null, null, null, null));
|
||||
|
||||
List<Book> bookList = GraphQlResponse.from(resultMono).toList("booksByCriteria", Book.class);
|
||||
assertThat(bookList).hasSize(2);
|
||||
@@ -107,7 +107,7 @@ public class SchemaMappingInvocationTests {
|
||||
"}";
|
||||
|
||||
AtomicReference<GraphQLContext> contextRef = new AtomicReference<>();
|
||||
RequestInput requestInput = new RequestInput(query, null, null, null);
|
||||
RequestInput requestInput = new RequestInput(query, null, null, null, null);
|
||||
requestInput.configureExecutionInput((executionInput, builder) -> {
|
||||
contextRef.set(executionInput.getGraphQLContext());
|
||||
return executionInput;
|
||||
@@ -134,7 +134,7 @@ public class SchemaMappingInvocationTests {
|
||||
"}";
|
||||
|
||||
Mono<ExecutionResult> resultMono = graphQlService()
|
||||
.execute(new RequestInput(operation, null, null, null));
|
||||
.execute(new RequestInput(operation, null, null, null, null));
|
||||
|
||||
Author author = GraphQlResponse.from(resultMono).toEntity("addAuthor", Author.class);
|
||||
assertThat(author.getId()).isEqualTo(99);
|
||||
@@ -152,7 +152,7 @@ public class SchemaMappingInvocationTests {
|
||||
"}";
|
||||
|
||||
Mono<ExecutionResult> resultMono = graphQlService()
|
||||
.execute(new RequestInput(operation, null, null, null));
|
||||
.execute(new RequestInput(operation, null, null, null, null));
|
||||
|
||||
Flux<Book> bookFlux = GraphQlResponse.forSubscription(resultMono)
|
||||
.map(response -> response.toEntity("bookSearch", Book.class));
|
||||
|
||||
@@ -163,7 +163,7 @@ public class SchemaMappingPrincipalMethodArgumentResolverTests {
|
||||
.toGraphQlService();
|
||||
|
||||
return Mono.delay(Duration.ofMillis(10))
|
||||
.flatMap(aLong -> graphQlService.execute(new RequestInput(op, null, null, null)))
|
||||
.flatMap(aLong -> graphQlService.execute(new RequestInput(op, null, null, null, null)))
|
||||
.contextWrite(contextWriter);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ public class BatchLoadingTests {
|
||||
.dataLoaders(this.registry)
|
||||
.toGraphQlService();
|
||||
|
||||
Mono<ExecutionResult> resultMono = service.execute(new RequestInput(query, null, null, null));
|
||||
Mono<ExecutionResult> resultMono = service.execute(new RequestInput(query, null, null, null, null));
|
||||
|
||||
List<Book> books = GraphQlResponse.from(resultMono).toList("booksByCriteria", Book.class);
|
||||
assertThat(books).hasSize(2);
|
||||
|
||||
@@ -85,7 +85,7 @@ public class ClassNameTypeResolverTests {
|
||||
|
||||
Mono<ExecutionResult> resultMono = graphQlSetup.queryFetcher("animals", env -> animalList)
|
||||
.toGraphQlService()
|
||||
.execute(new RequestInput(query, null, null, null));
|
||||
.execute(new RequestInput(query, null, null, null, null));
|
||||
|
||||
GraphQlResponse response = GraphQlResponse.from(resultMono);
|
||||
for (int i = 0; i < animalList.size(); i++) {
|
||||
@@ -128,7 +128,7 @@ public class ClassNameTypeResolverTests {
|
||||
Mono<ExecutionResult> result = graphQlSetup.queryFetcher("sightings", env -> animalAndPlantList)
|
||||
.typeResolver(typeResolver)
|
||||
.toGraphQlService()
|
||||
.execute(new RequestInput(query, null, null, null));
|
||||
.execute(new RequestInput(query, null, null, null, null));
|
||||
|
||||
GraphQlResponse response = GraphQlResponse.from(result);
|
||||
for (int i = 0; i < animalAndPlantList.size(); i++) {
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -59,6 +61,22 @@ public class GraphQlHttpHandlerTests {
|
||||
.isEqualTo("{\"data\":{\"greeting\":\"Hello in fr\"}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetExecutionId() {
|
||||
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { showId: String }")
|
||||
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
|
||||
.toHttpHandlerWebFlux();
|
||||
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/").build();
|
||||
|
||||
MockServerHttpResponse httpResponse = handleRequest(
|
||||
httpRequest, handler, Collections.singletonMap("query", "{showId}"));
|
||||
|
||||
DocumentContext document = JsonPath.parse(httpResponse.getBodyAsString().block());
|
||||
String id = document.read("data.showId", String.class);
|
||||
assertThat(id).isEqualTo(httpRequest.getId());
|
||||
}
|
||||
|
||||
private MockServerHttpResponse handleRequest(
|
||||
MockServerHttpRequest httpRequest, GraphQlHttpHandler handler, Map<String, String> body) {
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.util.Locale;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
@@ -52,13 +54,9 @@ public class GraphQlHttpHandlerTests {
|
||||
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
|
||||
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
|
||||
.toHttpHandler();
|
||||
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest("POST", "/");
|
||||
servletRequest.setContentType("application/json");
|
||||
servletRequest.setContent("{\"query\":\"{ greeting }\"}".getBytes(StandardCharsets.UTF_8));
|
||||
servletRequest.setAsyncSupported(true);
|
||||
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}");
|
||||
LocaleContextHolder.setLocale(Locale.FRENCH);
|
||||
|
||||
try {
|
||||
MockHttpServletResponse servletResponse = handleRequest(servletRequest, handler);
|
||||
|
||||
@@ -70,6 +68,28 @@ public class GraphQlHttpHandlerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetExecutionId() throws Exception {
|
||||
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { showId: ID! }")
|
||||
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
|
||||
.toHttpHandler();
|
||||
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ showId }\"}");
|
||||
|
||||
MockHttpServletResponse servletResponse = handleRequest(servletRequest, handler);
|
||||
DocumentContext document = JsonPath.parse(servletResponse.getContentAsString());
|
||||
String id = document.read("data.showId", String.class);
|
||||
assertThat(id).hasSize(8);
|
||||
}
|
||||
|
||||
private MockHttpServletRequest createServletRequest(String query) {
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest("POST", "/");
|
||||
servletRequest.setContentType("application/json");
|
||||
servletRequest.setContent(query.getBytes(StandardCharsets.UTF_8));
|
||||
servletRequest.setAsyncSupported(true);
|
||||
return servletRequest;
|
||||
}
|
||||
|
||||
private MockHttpServletResponse handleRequest(
|
||||
MockHttpServletRequest servletRequest, GraphQlHttpHandler handler) throws ServletException, IOException {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user