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:
Brian Clozel
2021-11-23 20:10:53 +01:00
parent d9404ea3d6
commit 2e4158cae8
14 changed files with 84 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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