Merge branch '1.0.x'
This commit is contained in:
@@ -155,30 +155,40 @@ public class GraphQlRSocketController {
|
||||
[[server-interception]]
|
||||
=== Interception
|
||||
|
||||
Transport handlers for <<server-http>> and <<server-websocket>> delegate to a
|
||||
`WebGraphQlInterceptor` chain with an `ExecutionGraphQlService` at the end which calls
|
||||
the GraphQL Java engine. Use this to access HTTP request details and customize the
|
||||
`ExecutionInput` for GraphQL Java.
|
||||
Server transports allow intercepting requests before and after the GraphQL Java engine is
|
||||
called to process a request.
|
||||
|
||||
For example, to extract HTTP request values and pass them to data fetchers:
|
||||
|
||||
[[server-interception-web]]
|
||||
==== `WebGraphQlInterceptor`
|
||||
|
||||
<<server-http>> and <<server-websocket>> transports invoke a chain of
|
||||
0 or more `WebGraphQlInterceptor`, followed by an `ExecutionGraphQlService` that calls
|
||||
the GraphQL Java engine. `WebGraphQlInterceptor` allows an application to intercept
|
||||
incoming requests and do one of the following:
|
||||
|
||||
- Check HTTP request details
|
||||
- Customize the `graphql.ExecutionInput`
|
||||
- Add HTTP response headers
|
||||
- Customize the `graphql.ExecutionResult`
|
||||
|
||||
For example, an interceptor can pass an HTTP request header to a `DataFetcher`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class HeaderInterceptor implements WebGraphQlInterceptor {
|
||||
class HeaderInterceptor implements WebGraphQlInterceptor { <1>
|
||||
|
||||
@Override
|
||||
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
|
||||
List<String> values = request.getHeaders().get("headerName");
|
||||
String value = request.getHeaders().getFirst("myHeader");
|
||||
request.configureExecutionInput((executionInput, builder) ->
|
||||
builder.graphQLContext(Collections.singletonMap("headerName", values)).build());
|
||||
builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
|
||||
return chain.next(request);
|
||||
}
|
||||
}
|
||||
|
||||
// Subsequent access from a controller
|
||||
|
||||
@Controller
|
||||
class MyController {
|
||||
class MyController { <2>
|
||||
|
||||
@QueryMapping
|
||||
Person person(@ContextValue String myHeader) {
|
||||
@@ -186,8 +196,10 @@ class MyController {
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Interceptor adds HTTP request header value into GraphQLContext
|
||||
<2> Data controller method accesses the value
|
||||
|
||||
Or reversely, add values to the `GraphQLContext` and use them to update the HTTP response:
|
||||
Reversely, an interceptor can access values added to the `GraphQLContext` by a controller:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -195,7 +207,7 @@ Or reversely, add values to the `GraphQLContext` and use them to update the HTTP
|
||||
class MyController {
|
||||
|
||||
@QueryMapping
|
||||
Person person(GraphQLContext context) {
|
||||
Person person(GraphQLContext context) { <1>
|
||||
context.put("cookieName", "123");
|
||||
}
|
||||
}
|
||||
@@ -205,7 +217,7 @@ class MyController {
|
||||
class HeaderInterceptor implements WebGraphQlInterceptor {
|
||||
|
||||
@Override
|
||||
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
|
||||
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { <2>
|
||||
return chain.next(request).doOnNext(response -> {
|
||||
String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
|
||||
ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
|
||||
@@ -214,13 +226,52 @@ class HeaderInterceptor implements WebGraphQlInterceptor {
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Controller adds value to the `GraphQLContext`
|
||||
<2> Interceptor uses the value to add an HTTP response header
|
||||
|
||||
The `WebGraphQlInterceptor` chain can be updated through the `WebGraphQlHandler` builder,
|
||||
and the Boot starter uses this, see Boot's section on
|
||||
`WebGraphQlHandler` can modify the `ExecutionResult`, for example, to inspect and modify
|
||||
request validation errors that are raised before execution begins and which cannot be
|
||||
handled with a `DataFetcherExceptionResolver`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
static class RequestErrorInterceptor implements WebGraphQlInterceptor {
|
||||
|
||||
@Override
|
||||
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
|
||||
return chain.next(request).map(response -> {
|
||||
if (response.isValid()) {
|
||||
return response; <1>
|
||||
}
|
||||
|
||||
List<GraphQLError> errors = response.getErrors().stream() <2>
|
||||
.map(error -> {
|
||||
GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
|
||||
// ...
|
||||
return builder.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return response.transform(builder -> builder.errors(errors).build()); <3>
|
||||
});
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Return the same if `ExecutionResult` has a "data" key with non-null value
|
||||
<2> Check and transform the GraphQL errors
|
||||
<3> Update the `ExecutionResult` with the modified errors
|
||||
|
||||
Use `WebGraphQlHandler` to configure the `WebGraphQlInterceptor` chain. This is supported
|
||||
by the Boot starter, see
|
||||
{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints].
|
||||
|
||||
The <<server-rsocket>> transport handler delegates to a similar `GraphQlInterceptor`
|
||||
chain that you can use to intercept GraphQL over RSocket requests.
|
||||
|
||||
[[server-interception-rsocket]]
|
||||
==== `RSocketQlInterceptor`
|
||||
|
||||
Similar to <<server-interception-web>>, an `RSocketQlInterceptor` allows intercepting
|
||||
GraphQL over RSocket requests before and after GraphQL Java engine execution. You can use
|
||||
this to customize the `graphql.ExecutionInput` and the `graphql.ExecutionResult`.
|
||||
|
||||
|
||||
|
||||
@@ -567,6 +618,19 @@ error details.
|
||||
Unresolved exception are logged at ERROR level along with the `executionId` to correlate
|
||||
to the error sent to the client. Resolved exceptions are logged at DEBUG level.
|
||||
|
||||
[[execution-exceptions-request]]
|
||||
==== Request Exceptions
|
||||
|
||||
The GraphQL Java engine may run into validation or other errors when parsing the request
|
||||
and that in turn prevent request execution. In such cases, the response contains a
|
||||
"data" key with `null` and one or more request-level "errors" that are global, i.e. not
|
||||
having a field path.
|
||||
|
||||
`DataFetcherExceptionResolver` cannot handle such global errors because they are raised
|
||||
before execution begins and before any `DataFetcher` is invoked. An application can use
|
||||
transport level interceptors to inspect and transform errors in the `ExecutionResult`.
|
||||
See examples under <<server-interception-web>>.
|
||||
|
||||
|
||||
[[execution-exceptions-subsctiption]]
|
||||
==== Subscription Exceptions
|
||||
|
||||
@@ -188,9 +188,10 @@ public class GraphQlArgumentBinder {
|
||||
return (type.resolve(Object.class).equals(Optional.class) ? Optional.ofNullable(value) : value);
|
||||
}
|
||||
|
||||
private boolean isApproximableCollectionType(Object rawValue) {
|
||||
return (CollectionFactory.isApproximableCollectionType(rawValue.getClass()) ||
|
||||
rawValue instanceof List); // it may be SingletonList
|
||||
private boolean isApproximableCollectionType(@Nullable Object rawValue) {
|
||||
return (rawValue != null &&
|
||||
(CollectionFactory.isApproximableCollectionType(rawValue.getClass()) ||
|
||||
rawValue instanceof List)); // it may be SingletonList
|
||||
}
|
||||
|
||||
@SuppressWarnings({"ConstantConditions", "unchecked"})
|
||||
@@ -283,12 +284,15 @@ public class GraphQlArgumentBinder {
|
||||
if (rawValue == null && methodParam.isOptional()) {
|
||||
args[i] = (paramTypes[i] == Optional.class ? Optional.empty() : null);
|
||||
}
|
||||
else if (rawValue != null && isApproximableCollectionType(rawValue)) {
|
||||
else if (isApproximableCollectionType(rawValue)) {
|
||||
ResolvableType elementType = ResolvableType.forMethodParameter(methodParam);
|
||||
args[i] = createCollection((Collection<Object>) rawValue, elementType, bindingResult, segments);
|
||||
}
|
||||
else if (rawValue instanceof Map) {
|
||||
args[i] = createValueOrNull((Map<String, Object>) rawValue, paramTypes[i], bindingResult, segments);
|
||||
boolean isOptional = (paramTypes[i] == Optional.class);
|
||||
Class<?> type = (isOptional ? methodParam.nestedIfOptional().getNestedParameterType() : paramTypes[i]);
|
||||
Object value = createValueOrNull((Map<String, Object>) rawValue, type, bindingResult, segments);
|
||||
args[i] = (isOptional ? Optional.ofNullable(value) : value);
|
||||
}
|
||||
else {
|
||||
args[i] = convertValue(rawValue, paramTypes[i], new TypeDescriptor(methodParam), bindingResult, segments);
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -32,6 +33,7 @@ import graphql.schema.DataFetchingEnvironmentImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
@@ -182,6 +184,26 @@ class GraphQlArgumentBinderTests {
|
||||
assertThat(((PrimaryConstructorItemBean) result).getAge()).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test
|
||||
void primaryConstructorWithOptionalBeanArgument() throws Exception {
|
||||
|
||||
GraphQlArgumentBinder argumentBinder =
|
||||
new GraphQlArgumentBinder(new DefaultFormattingConversionService());
|
||||
|
||||
Object result = argumentBinder.bind(
|
||||
environment(
|
||||
"{\"key\":{" +
|
||||
"\"item\":{\"name\":\"Item name\"}," +
|
||||
"\"name\":\"Hello\"," +
|
||||
"\"age\":\"30\"}}"),
|
||||
"key",
|
||||
ResolvableType.forClass(PrimaryConstructorOptionalItemBean.class));
|
||||
|
||||
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorOptionalItemBean.class);
|
||||
assertThat(((PrimaryConstructorOptionalItemBean) result).getItem().get().getName()).isEqualTo("Item name");
|
||||
assertThat(((PrimaryConstructorOptionalItemBean) result).getName().get()).isEqualTo("Hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void primaryConstructorWithNestedBeanList() throws Exception {
|
||||
|
||||
@@ -390,6 +412,28 @@ class GraphQlArgumentBinderTests {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
static class PrimaryConstructorOptionalItemBean {
|
||||
|
||||
private final Optional<String> name;
|
||||
|
||||
private final Optional<Item> item;
|
||||
|
||||
public PrimaryConstructorOptionalItemBean(Optional<String> name, Optional<Item> item) {
|
||||
this.name = name;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public Optional<String> getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public Optional<Item> getItem() {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class NoPrimaryConstructorBean {
|
||||
|
||||
NoPrimaryConstructorBean(String name) {
|
||||
|
||||
Reference in New Issue
Block a user