Migrate Structure

This commit is contained in:
Rob Winch
2023-07-20 20:20:06 -05:00
committed by Rossen Stoyanchev
parent edda403220
commit 49446b7171
11 changed files with 0 additions and 0 deletions

View File

@@ -1,26 +0,0 @@
:chomp: default headers packages
:docs-site: https://docs.spring.io
:idprefix:
:idseparator: -
:toc: left
:toclevels: 4
:tabsize: 4
:numbered:
:sectanchors:
:sectnums:
:hide-uri-scheme:
:docs-java: {docdir}/../../main/java/org/springframework/graphql/docs
:docs-kotlin: {docdir}/../../main/kotlin/org/springframework/graphql/docs
:docs-resources: {docdir}/../../main/resources
:github-tag: main
:github-repo: spring-projects/spring-graphql
:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag}
:github-issues: https://github.com/{github-repo}/issues/
:github-main-branch: https://github.com/{github-repo}/tree/main
:github-10x-branch: https://github.com/{github-repo}/tree/1.0.x
:github-wiki: https://github.com/{github-repo}/wiki
:graphql-java-docs: https://www.graphql-java.com/documentation
:javadoc: https://docs.spring.io/spring-graphql/docs/{spring-graphql-version}/api
// version attributes from main build.gradle
:spring-framework-ref-docs: https://docs.spring.io/spring-framework/reference
:spring-boot-ref-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference/html

View File

@@ -1,458 +0,0 @@
[[client]]
= Client
Spring for GraphQL includes client support for executing GraphQL requests over HTTP,
WebSocket, and RSocket.
[[client.graphqlclient]]
== `GraphQlClient`
`GraphQlClient` is a contract that declares a common workflow for GraphQL requests that is
independent of the underlying transport. That means requests are executed with the same API
no matter what the underlying transport, and anything transport specific is configured at
build time.
To create a `GraphQlClient` you need one of the following extensions:
- <<client.httpgraphqlclient, HttpGraphQlClient>>
- <<client.websocketgraphqlclient, WebSocketGraphQlClient>>
- <<client.rsocketgraphqlclient, RSocketGraphQlClient>>
Each defines a `Builder` with options relevant to the transport. All builders extend
from a common, base GraphQlClient <<client.graphqlclient.builder, `Builder`>> with options
relevant to all extensions.
Once you have a `GraphQlClient` you can begin to make <<client.requests, requests>>.
[[client.httpgraphqlclient]]
=== HTTP
`HttpGraphQlClient` uses
{spring-framework-ref-docs}/web/webflux-webclient.html[WebClient] to execute
GraphQL requests over HTTP.
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
----
Once `HttpGraphQlClient` is created, you can begin to
<<client.requests, execute requests>> using the same API, independent of the underlying
transport. If you need to change any transport specific details, use `mutate()` on an
existing `HttpGraphQlClient` to create a new instance with customized settings:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Perform requests with graphQlClient...
HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Perform requests with anotherGraphQlClient...
----
[[client.websocketgraphqlclient]]
=== WebSocket
`WebSocketGraphQlClient` executes GraphQL requests over a shared WebSocket connection.
It is built using the
{spring-framework-ref-docs}/web/webflux-websocket.html#webflux-websocket-client[WebSocketClient]
from Spring WebFlux and you can create it as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
String url = "wss://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();
----
In contrast to `HttpGraphQlClient`, the `WebSocketGraphQlClient` is connection oriented,
which means it needs to establish a connection before making any requests. As you begin
to make requests, the connection is established transparently. Alternatively, use the
client's `start()` method to establish the connection explicitly before any requests.
In addition to being connection-oriented, `WebSocketGraphQlClient` is also multiplexed.
It maintains a single, shared connection for all requests. If the connection is lost,
it is re-established on the next request or if `start()` is called again. You can also
use the client's `stop()` method which cancels in-progress requests, closes the
connection, and rejects new requests.
TIP: Use a single `WebSocketGraphQlClient` instance for each server in order to have a
single, shared connection for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
Once `WebSocketGraphQlClient` is created, you can begin to
<<client.requests, execute requests>> using the same API, independent of the underlying
transport. If you need to change any transport specific details, use `mutate()` on an
existing `WebSocketGraphQlClient` to create a new instance with customized settings:
[source,java,indent=0,subs="verbatim,quotes"]
----
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use graphQlClient...
WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherGraphQlClient...
----
[[client.websocketgraphqlclient.interceptor]]
==== Interceptor
The https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md[GraphQL over WebSocket]
protocol defines a number of connection oriented messages in addition to executing
requests. For example, a client sends `"connection_init"` and the server responds with
`"connection_ack"` at the start of a connection.
For WebSocket transport specific interception, you can create a
`WebSocketGraphQlClientInterceptor`:
[source,java,indent=0,subs="verbatim,quotes"]
----
static class MyInterceptor implements WebSocketGraphQlClientInterceptor {
@Override
public Mono<Object> connectionInitPayload() {
// ... the "connection_init" payload to send
}
@Override
public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
// ... the "connection_ack" payload received
}
}
----
<<client.interception,Register>> the above interceptor as any other
`GraphQlClientInterceptor` and use it also to intercept GraphQL requests, but note there
can be at most one interceptor of type `WebSocketGraphQlClientInterceptor`.
[[client.rsocketgraphqlclient]]
=== RSocket
`RSocketGraphQlClient` uses
{spring-framework-ref-docs}/rsocket.html#rsocket-requester[RSocketRequester]
to execute GraphQL requests over RSocket requests.
[source,java,indent=0,subs="verbatim,quotes"]
----
URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.clientTransport(transport)
.build();
----
In contrast to `HttpGraphQlClient`, the `RSocketGraphQlClient` is connection oriented,
which means it needs to establish a session before making any requests. As you begin
to make requests, the session is established transparently. Alternatively, use the
client's `start()` method to establish the session explicitly before any requests.
`RSocketGraphQlClient` is also multiplexed. It maintains a single, shared session for
all requests. If the session is lost, it is re-established on the next request or if
`start()` is called again. You can also use the client's `stop()` method which cancels
in-progress requests, closes the session, and rejects new requests.
TIP: Use a single `RSocketGraphQlClient` instance for each server in order to have a
single, shared session for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
Once `RSocketGraphQlClient` is created, you can begin to
<<client.requests, execute requests>> using the same API, independent of the underlying
transport.
[[client.graphqlclient.builder]]
=== Builder
`GraphQlClient` defines a parent `Builder` with common configuration options for the
builders of all extensions. Currently, it has lets you configure:
- `DocumentSource` strategy to load the document for a request from a file
- <<client.interception>> of executed requests
[[client.requests]]
== Requests
Once you have a <<client.graphqlclient>>, you can begin to perform requests via
<<client.requests.retrieve, retrieve()>> or <<client.requests.execute, execute()>>
where the former is only a shortcut for the latter.
[[client.requests.retrieve]]
=== Retrieve
The below retrieves and decodes the data for a query:
[source,java,indent=0,subs="verbatim,quotes"]
----
String document = "{" +
" project(slug:\"spring-framework\") {" +
" name" +
" releases {" +
" version" +
" }"+
" }" +
"}";
Mono<Project> projectMono = graphQlClient.document(document) <1>
.retrieve("project") <2>
.toEntity(Project.class); <3>
----
<1> The operation to perform.
<2> The path under the "data" key in the response map to decode from.
<3> Decode the data at the path to the target type.
The input document is a `String` that could be a literal or produced through a code
generated request object. You can also define documents in files and use a
<<client.requests.document-source>> to resole them by file name.
The path is relative to the "data" key and uses a simple dot (".") separated notation
for nested fields with optional array indices for list elements, e.g. `"project.name"`
or `"project.releases[0].version"`.
Decoding can result in `FieldAccessException` if the given path is not present, or the
field value is `null` and has an error. `FieldAccessException` provides access to the
response and the field:
[source,java,indent=0,subs="verbatim,quotes"]
----
Mono<Project> projectMono = graphQlClient.document(document)
.retrieve("project")
.toEntity(Project.class)
.onErrorResume(FieldAccessException.class, ex -> {
ClientGraphQlResponse response = ex.getResponse();
// ...
ClientResponseField field = ex.getField();
// ...
});
----
[[client.requests.execute]]
=== Execute
<<client.requests.retrieve>> is only a shortcut to decode from a single path in the
response map. For more control, use the `execute` method and handle the response:
For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
Mono<Project> projectMono = graphQlClient.document(document)
.execute()
.map(response -> {
if (!response.isValid()) {
// Request failure... <1>
}
ClientResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure... <2>
}
else {
// Optional field set to null... <3>
}
}
return field.toEntity(Project.class); <4>
});
----
<1> The response does not have data, only errors
<2> Field that is `null` and has an associated error
<3> Field that was set to `null` by its `DataFetcher`
<4> Decode the data at the given path
[[client.requests.document-source]]
=== Document Source
The document for a request is a `String` that may be defined in a local variable or
constant, or it may be produced through a code generated request object.
You can also create document files with extensions `.graphql` or `.gql` under
`"graphql-documents/"` on the classpath and refer to them by file name.
For example, given a file called `projectReleases.graphql` in
`src/main/resources/graphql-documents`, with content:
[source,graphql,indent=0,subs="verbatim,quotes"]
.src/main/resources/graphql-documents/projectReleases.graphql
----
query projectReleases($slug: ID!) {
project(slug: $slug) {
name
releases {
version
}
}
}
----
You can then:
[source,java,indent=0,subs="verbatim,quotes"]
----
Mono<Project> projectMono = graphQlClient.documentName("projectReleases") <1>
.variable("slug", "spring-framework") <2>
.retrieve()
.toEntity(Project.class);
----
<1> Load the document from "projectReleases.graphql"
<2> Provide variable values.
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
You can use the `GraphQlClient` <<client.graphqlclient.builder>> to customize the
`DocumentSource` for loading documents by names.
[[client.subscriptions]]
== Subscription Requests
`GraphQlClient` can execute subscriptions over transports that support it. Only
the WebSocket and RSocket transports support GraphQL subscriptions, so you'll need to
create a <<client.websocketgraphqlclient,WebSocketGraphQlClient>> or
<<client.rsocketgraphqlclient,RSocketGraphQlClient>>.
[[client.subscriptions.retrieve]]
=== Retrieve
To start a subscription stream, use `retrieveSubscription` which is similar to
<<client.requests.retrieve,retrieve>> for a single response but returning a stream of
responses, each decoded to some data:
[source,java,indent=0,subs="verbatim,quotes"]
----
Flux<String> greetingFlux = client.document("subscription { greetings }")
.retrieveSubscription("greeting")
.toEntity(String.class);
----
The `Flux` may terminate with `SubscriptionErrorException` if the subscription ends from
the server side with an "error" message. The exception provides access to GraphQL errors
decoded from the "error" message.
The `Flux` may termiate with `GraphQlTransportException` such as
`WebSocketDisconnectedException` if the underlying connection is closed or lost. In that
case you can use the `retry` operator to restart the subscription.
To end the subscription from the client side, the `Flux` must be cancelled, and in turn
the WebSocket transport sends a "complete" message to the server. How to cancel the
`Flux` depends on how it is used. Some operators such as `take` or `timeout` themselves
cancel the `Flux`. If you subscribe to the `Flux` with a `Subscriber`, you can get a
reference to the `Subscription` and cancel through it. The `onSubscribe` operator also
provides access to the `Subscription`.
[[client.subscriptions.execute]]
=== Execute
<<client.subscriptions.retrieve>> is only a shortcut to decode from a single path in each
response map. For more control, use the `executeSubscription` method and handle each
response directly:
[source,java,indent=0,subs="verbatim,quotes"]
----
Flux<String> greetingFlux = client.document("subscription { greetings }")
.executeSubscription()
.map(response -> {
if (!response.isValid()) {
// Request failure...
}
ClientResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure...
}
else {
// Optional field set to null... <3>
}
}
return field.toEntity(String.class)
});
----
[[client.interception]]
== Interception
You create a `GraphQlClientInterceptor` to intercept all requests through a client:
[source,java,indent=0,subs="verbatim,quotes"]
----
static class MyInterceptor implements GraphQlClientInterceptor {
@Override
public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
// ...
return chain.next(request);
}
@Override
public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
// ...
return chain.next(request);
}
}
----
Once the interceptor is created, register it through the client builder:
[source,java,indent=0,subs="verbatim,quotes"]
----
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.interceptor(new MyInterceptor())
.build();
----

View File

@@ -1,756 +0,0 @@
[[controllers]]
= Annotated Controllers
Spring for GraphQL provides an annotation-based programming model where `@Controller`
components use annotations to declare handler methods with flexible method signatures to
fetch the data for specific GraphQL fields. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class GreetingController {
@QueryMapping // <1>
public String hello() { // <2>
return "Hello, world!";
}
}
----
<1> Bind this method to a query, i.e. a field under the Query type.
<2> Determine the query from the method name if not declared on the annotation.
Spring for GraphQL uses `RuntimeWiring.Builder` to register the above handler method as a
`graphql.schema.DataFetcher` for the query named "hello".
[[controllers-declaration]]
== Declaration
You can define `@Controller` beans as standard Spring bean definitions. The
`@Controller` stereotype allows for auto-detection, aligned with Spring general
support for detecting `@Controller` and `@Component` classes on the classpath and
auto-registering bean definitions for them. It also acts as a stereotype for the annotated
class, indicating its role as a data fetching component in a GraphQL application.
`AnnotatedControllerConfigurer` detects `@Controller` beans and registers their
annotated handler methods as ``DataFetcher``s via `RuntimeWiring.Builder`. It is an
implementation of `RuntimeWiringConfigurer` which can be added to `GraphQlSource.Builder`.
The <<boot-starter>> automatically declares `AnnotatedControllerConfigurer` as a bean
and adds all `RuntimeWiringConfigurer` beans to `GraphQlSource.Builder` and that enables
support for annotated ``DataFetcher``s, see the
{spring-boot-ref-docs}/web.html#web.graphql.runtimewiring[GraphQL RuntimeWiring] section
in the Boot starter documentation.
[[controllers.schema-mapping]]
== `@SchemaMapping`
The `@SchemaMapping` annotation maps a handler method to a field in the GraphQL schema
and declares it to be the `DataFetcher` for that field. The annotation can specify the
parent type name, and the field name:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@SchemaMapping(typeName="Book", field="author")
public Author getAuthor(Book book) {
// ...
}
}
----
The `@SchemaMapping` annotation can also leave out those attributes, in which case the
field name defaults to the method name, while the type name defaults to the simple class
name of the source/parent object injected into the method. For example, the below
defaults to type "Book" and field "author":
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
----
The `@SchemaMapping` annotation can be declared at the class level to specify a default
type name for all handler methods in the class.
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
@SchemaMapping(typeName="Book")
public class BookController {
// @SchemaMapping methods for fields of the "Book" type
}
----
`@QueryMapping`, `@MutationMapping`, and `@SubscriptionMapping` are meta annotations that
are themselves annotated with `@SchemaMapping` and have the typeName preset to `Query`,
`Mutation`, or `Subscription` respectively. Effectively, these are shortcut annotations
for fields under the Query, Mutation, and Subscription types respectively. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
@SubscriptionMapping
public Flux<Book> newPublications() {
// ...
}
}
----
`@SchemaMapping` handler methods have flexible signatures and can choose from a range of
method arguments and return values..
[[controllers.schema-mapping.signature]]
=== Method Signature
Schema mapping handler methods can have any of the following method arguments:
[cols="1,2"]
|===
| Method Argument | Description
| `@Argument`
| For access to a named field argument bound to a higher-level, typed Object.
See <<controllers.schema-mapping.argument>>.
| `@Argument Map<String, Object>`
| For access to the raw argument value.
See <<controllers.schema-mapping.argument>>.
| `ArgumentValue`
| For access to a named field argument bound to a higher-level, typed Object along
with a flag to indicate if the input argument was omitted vs set to `null`.
See <<controllers.schema-mapping.argument-value>>.
| `@Arguments`
| For access to all field arguments bound to a higher-level, typed Object.
See <<controllers.schema-mapping.arguments>>.
| `@Arguments Map<String, Object>`
| For access to the raw map of arguments.
| `@ProjectedPayload` Interface
| For access to field arguments through a project interface.
See <<controllers.schema-mapping.projectedpayload.argument>>.
| "Source"
| For access to the source (i.e. parent/container) instance of the field.
See <<controllers.schema-mapping.source>>.
| `Subrange` and `ScrollSubrange`
| For access to pagination arguments.
See <<execution.pagination>>, <<data.pagination.scroll>>, <<controllers.schema-mapping.subrange>>.
| `Sort`
| For access to sort details.
See <<execution.pagination>>, <<controllers.schema-mapping.sort>>.
| `DataLoader`
| For access to a `DataLoader` in the `DataLoaderRegistry`.
See <<controllers.schema-mapping.data-loader>>.
| `@ContextValue`
| For access to an attribute from the main `GraphQLContext` in `DataFetchingEnvironment`.
| `@LocalContextValue`
| For access to an attribute from the local `GraphQLContext` in `DataFetchingEnvironment`.
| `GraphQLContext`
| For access to the context from the `DataFetchingEnvironment`.
| `java.security.Principal`
| Obtained from the Spring Security context, if available.
| `@AuthenticationPrincipal`
| For access to `Authentication#getPrincipal()` from the Spring Security context.
| `DataFetchingFieldSelectionSet`
| For access to the selection set for the query through the `DataFetchingEnvironment`.
| `Locale`, `Optional<Locale>`
| For access to the `Locale` from the `DataFetchingEnvironment`.
| `DataFetchingEnvironment`
| For direct access to the underlying `DataFetchingEnvironment`.
|===
Schema mapping handler methods can return:
- A resolved value of any type.
- `Mono` and `Flux` for asynchronous value(s). Supported for controller methods and for
any `DataFetcher` as described in <<execution.reactive-datafetcher>>.
- `java.util.concurrent.Callable` to have the value(s) produced asynchronously.
For this to work, `AnnotatedControllerConfigurer` must be configured with an `Executor`.
[[controllers.schema-mapping.argument]]
=== `@Argument`
In GraphQL Java, `DataFetchingEnvironment` provides access to a map of field-specific
argument values. The values can be simple scalar values (e.g. String, Long), a `Map` of
values for more complex input, or a `List` of values.
Use the `@Argument` annotation to have an argument bound to a target object and
injected into the handler method. Binding is performed by mapping argument values to a
primary data constructor of the expected method parameter type, or by using a default
constructor to create the object and then map argument values to its properties. This is
repeated recursively, using all nested argument values and creating nested target objects
accordingly. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
}
----
TIP: If the target object doesn't have setters, and you can't change that, you can use a
property on `AnnotatedControllerConfigurer` to allow falling back on binding via direct
field access.
By default, if the method parameter name is available (requires the `-parameters` compiler
flag with Java 8+ or debugging info from the compiler), it is used to look up the argument.
If needed, you can customize the name through the annotation, e.g. `@Argument("bookInput")`.
TIP: The `@Argument` annotation does not have a "required" flag, nor the option to
specify a default value. Both of these can be specified at the GraphQL schema level and
are enforced by GraphQL Java.
If binding fails, a `BindException` is raised with binding issues accumulated as field
errors where the `field` of each error is the argument path where the issue occurred.
You can use `@Argument` with a `Map<String, Object>` argument, to obtain the raw value of
the argument. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument Map<String, Object> bookInput) {
// ...
}
}
----
NOTE: Prior to 1.2, `@Argument Map<String, Object>` returned the full arguments map if
the annotation did not specify a name. After 1.2, `@Argument` with
`Map<String, Object>` always returns the raw argument value, matching either to the name
specified in the annotation, or to the parameter name. For access to the full arguments
map, please use <<controllers.schema-mapping.arguments>> instead.
[[controllers.schema-mapping.argument-value]]
=== `ArgumentValue`
By default, input arguments in GraphQL are nullable and optional, which means an argument
can be set to the `null` literal, or not provided at all. This distinction is useful for
partial updates with a mutation where the underlying data may also be, either set to
`null` or not changed at all accordingly. When using <<controllers.schema-mapping.argument>>
there is no way to make such a distinction, because you would get `null` or an empty
`Optional` in both cases.
If you want to know not whether a value was not provided at all, you can declare an
`ArgumentValue` method parameter, which is a simple container for the resulting value,
along with a flag to indicate whether the input argument was omitted altogether. You
can use this instead of `@Argument`, in which case the argument name is determined from
the method parameter name, or together with `@Argument` to specify the argument name.
For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@MutationMapping
public void addBook(ArgumentValue<BookInput> bookInput) {
if (!bookInput.isOmitted()) {
BookInput value = bookInput.value();
// ...
}
}
}
----
`ArgumentValue` is also supported as a field within the object structure of an `@Argument`
method parameter, either initialized via a constructor argument or via a setter, including
as a field of an object nested at any level below the top level object.
[[controllers.schema-mapping.arguments]]
=== `@Arguments`
Use the `@Arguments` annotation, if you want to bind the full arguments map onto a single
target Object, in contrast to `@Argument`, which binds a specific, named argument.
For example, `@Argument BookInput bookInput` uses the value of the argument "bookInput"
to initialize `BookInput`, while `@Arguments` uses the full arguments map and in that
case, top-level arguments are bound to `BookInput` properties.
You can use `@Arguments` with a `Map<String, Object>` argument, to obtain the raw map of
all argument values.
[[controllers.schema-mapping.projectedpayload.argument]]
=== `@ProjectedPayload` Interface
As an alternative to using complete Objects with <<controllers.schema-mapping.argument>>,
you can also use a projection interface to access GraphQL request arguments through a
well-defined, minimal interface. Argument projections are provided by
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces[Spring Data's Interface projections]
when Spring Data is on the class path.
To make use of this, create an interface annotated with `@ProjectedPayload` and declare
it as a controller method parameter. If the parameter is annotated with `@Argument`,
it applies to an individual argument within the `DataFetchingEnvironment.getArguments()`
map. When declared without `@Argument`, the projection works on top-level arguments in
the complete arguments map.
For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Book bookById(BookIdProjection bookId) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInputProjection bookInput) {
// ...
}
}
@ProjectedPayload
interface BookIdProjection {
Long getId();
}
@ProjectedPayload
interface BookInputProjection {
String getName();
@Value("#{target.author + ' ' + target.name}")
String getAuthorAndName();
}
----
[[controllers.schema-mapping.source]]
=== Source
In GraphQL Java, the `DataFetchingEnvironment` provides access to the source (i.e.
parent/container) instance of the field. To access this, simply declare a method parameter
of the expected target type.
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
----
The source method argument also helps to determine the type name for the mapping.
If the simple name of the Java class matches the GraphQL type, then there is no need to
explicitly specify the type name in the `@SchemaMapping` annotation.
[TIP]
====
A <<controllers.batch-mapping>> handler method can batch load all authors for a query,
given a list of source/parent books objects.
====
[[controllers.schema-mapping.subrange]]
=== `Subrange`
When there is a <<execution.pagination.cursor.strategy>> bean in Spring configuration,
controller methods support a `Subrange<P>` argument where `<P>` is a relative position
converted from a cursor. For Spring Data, `ScrollSubrange` exposes `ScrollPosition`.
For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(ScrollSubrange subrange) {
ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial())
int count = subrange.count().orElse(20);
// ...
}
}
----
See <<execution.pagination>> for an overview of pagination and of built-in mechanisms.
[[controllers.schema-mapping.sort]]
=== `Sort`
When there is a <<data.pagination.scroll,SortStrategy>> bean in Spring configuration, controller
methods support `Sort` as a method argument. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(Optional<Sort> optionalSort) {
Sort sort = optionalSort.orElse(Sort.by(..));
}
}
----
[[controllers.schema-mapping.data-loader]]
=== `DataLoader`
When you register a batch loading function for an entity, as explained in
<<execution.batching>>, you can access the `DataLoader` for the entity by declaring a
method argument of type `DataLoader` and use it to load the entity:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
----
By default, `BatchLoaderRegistry` uses the full class name of the value type (e.g. the
class name for `Author`) for the key of the registration, and therefore simply declaring
the `DataLoader` method argument with generic types provides enough information
to locate it in the `DataLoaderRegistry`. As a fallback, the `DataLoader` method argument
resolver will also try the method argument name as the key but typically that should not
be necessary.
Note that for many cases with loading related entities, where the `@SchemaMapping` simply
delegates to a `DataLoader`, you can reduce boilerplate by using a
<<controllers.batch-mapping,@BatchMapping>> method as described in the next section.
[[controllers.schema-mapping.validation]]
=== Validation
When a `javax.validation.Validator` bean is found, `AnnotatedControllerConfigurer` enables support for
{spring-framework-ref-docs}/core/validation/beanvalidation.html#validation-beanvalidation-overview[Bean Validation]
on annotated controller methods. Typically, the bean is of type `LocalValidatorFactoryBean`.
Bean validation lets you declare constraints on types:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class BookInput {
@NotNull
private String title;
@NotNull
@Size(max=13)
private String isbn;
}
----
You can then annotate a controller method parameter with `@Valid` to validate it before
method invocation:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument @Valid BookInput bookInput) {
// ...
}
}
----
If an error occurs during validation, a `ConstraintViolationException` is raised.
You can use the <<execution.exceptions>> chain to decide how to present that to clients
by turning it into an error to include in the GraphQL response.
TIP: In addition to `@Valid`, you can also use Spring's `@Validated` that allows
specifying validation groups.
Bean validation is useful for <<controllers.schema-mapping.argument>>,
<<controllers.schema-mapping.arguments>>, and
<<controllers.schema-mapping.projectedpayload.argument,@ProjectedPayload>>
method parameters, but applies more generally to any method parameter.
[WARNING]
.Validation and Kotlin Coroutines
====
Hibernate Validator is not compatible with Kotlin Coroutine methods and fails when
introspecting their method parameters. Please see
https://github.com/spring-projects/spring-graphql/issues/344#issuecomment-1082814093[spring-projects/spring-graphql#344 (comment)]
for links to relevant issues and a suggested workaround.
====
[[controllers.batch-mapping]]
== `@BatchMapping`
<<execution.batching>> addresses the N+1 select problem through the use of an
`org.dataloader.DataLoader` to defer the loading of individual entity instances, so they
can be loaded together. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
----
For the straight-forward case of loading an associated entity, shown above, the
`@SchemaMapping` method does nothing more than delegate to the `DataLoader`. This is
boilerplate that can be avoided with a `@BatchMapping` method. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
// ...
}
}
----
The above becomes a batch loading function in the `BatchLoaderRegistry`
where keys are `Book` instances and the loaded values their authors. In addition, a
`DataFetcher` is also transparently bound to the `author` field of the type `Book`, which
simply delegates to the `DataLoader` for authors, given its source/parent `Book` instance.
[TIP]
====
To be used as a unique key, `Book` must implement `hashcode` and `equals`.
====
By default, the field name defaults to the method name, while the type name defaults to
the simple class name of the input `List` element type. Both can be customized through
annotation attributes. The type name can also be inherited from a class level
`@SchemaMapping`.
[[controllers.batch-mapping.signature]]
=== Method Signature
Batch mapping methods support the following arguments:
[cols="1,2"]
|===
| Method Argument | Description
| `List<K>`
| The source/parent objects.
| `java.security.Principal`
| Obtained from Spring Security context, if available.
| `@ContextValue`
| For access to a value from the `GraphQLContext` of `BatchLoaderEnvironment`,
which is the same context as the one from the `DataFetchingEnvironment`.
| `GraphQLContext`
| For access to the context from the `BatchLoaderEnvironment`,
which is the same context as the one from the `DataFetchingEnvironment`.
| `BatchLoaderEnvironment`
| The environment that is available in GraphQL Java to a
`org.dataloader.BatchLoaderWithContext`.
|===
Batch mapping methods can return:
[cols="1,2"]
|===
| Return Type | Description
| `Mono<Map<K,V>>`
| A map with parent objects as keys, and batch loaded objects as values.
| `Flux<V>`
| A sequence of batch loaded objects that must be in the same order as the source/parent
objects passed into the method.
| `Map<K,V>`, `Collection<V>`
| Imperative variants, e.g. without remote calls to make.
| `Callable<Map<K,V>>`, `Callable<Collection<V>>`
| Imperative variants to be invoked asynchronously. For this to work,
`AnnotatedControllerConfigurer` must be configured with an `Executor`.
|===
[[controllers.exception-handler]]
== `@GraphQlExceptionHandler`
Use `@GraphQlExceptionHandler` methods to handle exceptions from data fetching with a
flexible <<controllers.exception-handler.signature,method signature>>. When declared in a
controller, exception handler methods apply to exceptions from the same controller:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@GraphQlExceptionHandler
public GraphQLError handle(BindException ex) {
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
}
}
----
When declared in an `@ControllerAdvice`, exception handler methods apply across controllers:
[source,java,indent=0,subs="verbatim,quotes"]
----
@ControllerAdvice
public class GlobalExceptionHandler {
@GraphQlExceptionHandler
public GraphQLError handle(BindException ex) {
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
}
}
----
Exception handling via `@GraphQlExceptionHandler` methods is applied automatically to
controller invocations. To handle exceptions from other `graphql.schema.DataFetcher`
implementations, not based on controller methods, obtain a
`DataFetcherExceptionResolver` from `AnnotatedControllerConfigurer`, and register it in
`GraphQlSource.Builder` as a <<execution.exceptions,DataFetcherExceptionResolver>>.
[[controllers.exception-handler.signature]]
=== Method Signature
Exception handler methods support a flexible method signature with method arguments
resolved from a `DataFetchingEnvironment,` and matching to those of
<<controllers.schema-mapping.arguments,@SchemaMapping methods>>.
Supported return types are listed below:
[cols="1,2"]
|===
| Return Type | Description
| `graphql.GraphQLError`
| Resolve the exception to a single field error.
| `Collection<GraphQLError>`
| Resolve the exception to multiple field errors.
| `void`
| Resolve the exception without response errors.
| `Object`
| Resolve the exception to a single error, to multiple errors, or none.
The return value must be `GraphQLError`, `Collection<GraphQLError>`, or `null`.
| `Mono<T>`
| For asynchronous resolution where `<T>` is one of the supported, synchronous, return types.
|===

View File

@@ -1,482 +0,0 @@
[[data]]
= Data Integration
Spring for GraphQL lets you leverage existing Spring technology, following common
programming models to expose underlying data sources through GraphQL.
This section discusses an integration layer for Spring Data that provides an easy way to
adapt a Querydsl or a Query by Example repository to a `DataFetcher`, including the
option for automated detection and GraphQL Query registration for repositories marked
with `@GraphQlRepository`.
[[data.querydsl]]
== Querydsl
Spring for GraphQL supports use of http://www.querydsl.com/[Querydsl] to fetch data through
the Spring Data
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions[Querydsl extension].
Querydsl provides a flexible yet typesafe approach to express query predicates by
generating a meta-model using annotation processors.
For example, declare a repository as `QuerydslPredicateExecutor`:
[source,java,indent=0,subs="verbatim,quotes"]
----
public interface AccountRepository extends Repository<Account, Long>,
QuerydslPredicateExecutor<Account> {
}
----
Then use it to create a `DataFetcher`:
[source,java,indent=0,subs="verbatim,quotes"]
----
// For single result queries
DataFetcher<Account> dataFetcher =
QuerydslDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QuerydslDataFetcher.builder(repository).many();
// For paginated queries
DataFetcher<Iterable<Account>> dataFetcher =
QuerydslDataFetcher.builder(repository).scrollable();
----
You can now register the above `DataFetcher` through a
<<execution.graphqlsource.runtimewiring-configurer>>.
The `DataFetcher` builds a Querydsl `Predicate` from GraphQL arguments, and uses it to
fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA, MongoDB, Neo4j, and LDAP.
NOTE: For a single argument that is a GraphQL input type, `QuerydslDataFetcher` nests one
level down, and uses the values from the argument sub-map.
If the repository is `ReactiveQuerydslPredicateExecutor`, the builder returns
`DataFetcher<Mono<Account>>` or `DataFetcher<Flux<Account>>`. Spring Data supports this
variant for MongoDB and Neo4j.
[[data.querydsl.build]]
=== Build Setup
To configure Querydsl in your build, follow the
https://querydsl.com/static/querydsl/latest/reference/html/ch02.html[official reference documentation]:
For example:
[source,groovy,indent=0,subs="verbatim,quotes,attributes",role="primary"]
.Gradle
----
dependencies {
//...
annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa",
'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final',
'javax.annotation:javax.annotation-api:1.3.2'
}
compileJava {
options.annotationProcessorPath = configurations.annotationProcessor
}
----
[source,xml,indent=0,subs="verbatim,quotes,attributes",role="secondary"]
.Maven
----
<dependencies>
<!-- ... -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jpa</classifier>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.2.Final</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
<plugins>
<!-- Annotation processor configuration -->
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>${apt-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
----
The {github-10x-branch}/samples/webmvc-http[webmvc-http] sample uses Querydsl for
`artifactRepositories`.
[[data.querydsl.customizations]]
=== Customizations
`QuerydslDataFetcher` supports customizing how GraphQL arguments are bound onto properties
to create a Querydsl `Predicate`. By default, arguments are bound as "is equal to" for
each available property. To customize that, you can use `QuerydslDataFetcher` builder
methods to provide a `QuerydslBinderCustomizer`.
A repository may itself be an instance of `QuerydslBinderCustomizer`. This is auto-detected
and transparently applied during <<data.querydsl.registration>>. However, when manually
building a `QuerydslDataFetcher` you will need to use builder methods to apply it.
`QuerydslDataFetcher` supports interface and DTO projections to transform query results
before returning these for further GraphQL processing.
TIP: To learn what projections are, please refer to the
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data docs].
To understand how to use projections in GraphQL, please see <<data.projections>>.
To use Spring Data projections with Querydsl repositories, create either a projection interface
or a target DTO class and configure it through the `projectAs` method to obtain a
`DataFetcher` producing the target type:
[source,java,indent=0,subs="verbatim,quotes"]
----
class Account {
String name, identifier, description;
Person owner;
}
interface AccountProjection {
String getName();
String getIdentifier();
}
// For single result queries
DataFetcher<AccountProjection> dataFetcher =
QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single();
// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
----
[[data.querydsl.registration]]
=== Auto-Registration
If a repository is annotated with `@GraphQlRepository`, it is automatically registered
for queries that do not already have a registered `DataFetcher` and whose return type
matches that of the repository domain type. This includes single value queries, multi-value
queries, and <<execution.pagination,paginated>> queries.
By default, the name of the GraphQL type returned by the query must match the simple name
of the repository domain type. If needed, you can use the `typeName` attribute of
`@GraphQlRepository` to specify the target GraphQL type name.
For paginated queries, the simple name of the repository domain type must match the
`Connection` type name without the `Connection` ending (e.g. `**Book**` matches
`**Books**Connection`). For auto-registration, pagination is offset-based with 20 items
per page.
Auto-registration detects if a given repository implements `QuerydslBinderCustomizer` and
transparently applies that through `QuerydslDataFetcher` builder methods.
Auto-registration is performed through a built-in `RuntimeWiringConfigurer` that can be
obtained from `QuerydslDataFetcher`. The <<boot-starter>> automatically
detects `@GraphQlRepository` beans and uses them to initialize the
`RuntimeWiringConfigurer` with.
Auto-registration applies <<data.querybyexample.customizations, customizations>>
by calling `customize(Builder)` on the repository instance if your repository
implements `QuerydslBuilderCustomizer` or `ReactiveQuerydslBuilderCustomizer`
respectively.
[[data.querybyexample]]
== Query by Example
Spring Data supports the use of
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#query-by-example[Query by Example]
to fetch data. Query by Example (QBE) is a simple querying technique that does not require
you to write queries through store-specific query languages.
Start by declaring a repository that is `QueryByExampleExecutor`:
[source,java,indent=0,subs="verbatim,quotes"]
----
public interface AccountRepository extends Repository<Account, Long>,
QueryByExampleExecutor<Account> {
}
----
Use `QueryByExampleDataFetcher` to turn the repository into a `DataFetcher`:
[source,java,indent=0,subs="verbatim,quotes"]
----
// For single result queries
DataFetcher<Account> dataFetcher =
QueryByExampleDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).many();
// For paginated queries
DataFetcher<Iterable<Account>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).scrollable();
----
You can now register the above `DataFetcher` through a
<<execution.graphqlsource.runtimewiring-configurer>>.
The `DataFetcher` uses the GraphQL arguments map to create the domain type of the
repository and use that as the example object to fetch data with. Spring Data supports
`QueryByExampleDataFetcher` for JPA, MongoDB, Neo4j, and Redis.
NOTE: For a single argument that is a GraphQL input type, `QueryByExampleDataFetcher`
nests one level down, and binds with the values from the argument sub-map.
If the repository is `ReactiveQueryByExampleExecutor`, the builder returns
`DataFetcher<Mono<Account>>` or `DataFetcher<Flux<Account>>`. Spring Data supports this
variant for MongoDB, Neo4j, Redis, and R2dbc.
[[data.querybyexample.build]]
=== Build Setup
Query by Example is already included in the Spring Data modules for the data stores where
it is supported, so no extra setup is required to enable it.
[[data.querybyexample.customizations]]
=== Customizations
`QueryByExampleDataFetcher` supports interface and DTO projections to transform query
results before returning these for further GraphQL processing.
TIP: To learn what projections are, please refer to the
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data documentation].
To understand the role of projections in GraphQL, please see <<data.projections>>.
To use Spring Data projections with Query by Example repositories, create either a projection interface
or a target DTO class and configure it through the `projectAs` method to obtain a
`DataFetcher` producing the target type:
[source,java,indent=0,subs="verbatim,quotes"]
----
class Account {
String name, identifier, description;
Person owner;
}
interface AccountProjection {
String getName();
String getIdentifier();
}
// For single result queries
DataFetcher<AccountProjection> dataFetcher =
QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single();
// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
----
[[data.querybyexample.registration]]
=== Auto-Registration
If a repository is annotated with `@GraphQlRepository`, it is automatically registered
for queries that do not already have a registered `DataFetcher` and whose return type
matches that of the repository domain type. This includes single value queries, multi-value
queries, and <<execution.pagination,paginated>> queries.
By default, the name of the GraphQL type returned by the query must match the simple name
of the repository domain type. If needed, you can use the `typeName` attribute of
`@GraphQlRepository` to specify the target GraphQL type name.
For paginated queries, the simple name of the repository domain type must match the
`Connection` type name without the `Connection` ending (e.g. `**Book**` matches
`**Books**Connection`). For auto-registration, pagination is offset-based with 20 items
per page.
Auto-registration is performed through a built-in `RuntimeWiringConfigurer` that can be
obtained from `QueryByExampleDataFetcher`. The <<boot-starter>> automatically
detects `@GraphQlRepository` beans and uses them to initialize the
`RuntimeWiringConfigurer` with.
Auto-registration applies <<data.querybyexample.customizations, customizations>>
by calling `customize(Builder)` on the repository instance if your repository
implements `QueryByExampleBuilderCustomizer` or
`ReactiveQueryByExampleBuilderCustomizer` respectively.
[[data.projections]]
== Selection Set vs Projections
A common question that arises is, how GraphQL selection sets compare to
https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections[Spring Data projections]
and what role does each play?
The short answer is that Spring for GraphQL is not a data gateway that translates GraphQL
queries directly into SQL or JSON queries. Instead, it lets you leverage existing Spring
technology and does not assume a one for one mapping between the GraphQL schema and the
underlying data model. That is why client-driven selection and server-side transformation
of the data model can play complementary roles.
To better understand, consider that Spring Data promotes domain-driven (DDD) design as
the recommended approach to manage complexity in the data layer. In DDD, it is important
to adhere to the constraints of an aggregate. By definition an aggregate is valid only if
loaded in its entirety, since a partially loaded aggregate may impose limitations on
aggregate functionality.
In Spring Data you can choose whether you want your aggregate be exposed as is, or
whether to apply transformations to the data model before returning it as a GraphQL
result. Sometimes it's enough to do the former, and by default the
<<data.querydsl>> and the <<data.querybyexample>> integrations turn the GraphQL
selection set into property path hints that the underlying Spring Data module uses to
limit the selection.
In other cases, it's useful to reduce or even transform the underlying data model in
order to adapt to the GraphQL schema. Spring Data supports this through Interface
and DTO Projections.
Interface projections define a fixed set of properties to expose where properties may or
may not be `null`, depending on the data store query result. There are two kinds of
interface projections both of which determine what properties to load from the underlying
data source:
- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.closed[Closed interface projections]
are helpful if you cannot partially materialize the aggregate object, but you still
want to expose a subset of properties.
- https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.open[Open interface projections]
leverage Spring's `@Value` annotation and
{spring-framework-ref-docs}/core/expressions.html[SpEL] expressions to apply lightweight
data transformations, such as concatenations, computations, or applying static functions
to a property.
DTO projections offer a higher level of customization as you can place transformation
code either in the constructor or in getter methods.
DTO projections materialize from a query where the individual properties are
determined by the projection itself. DTO projections are commonly used with full-args
constructors (e.g. Java records), and therefore they can only be constructed if all
required fields (or columns) are part of the database query result.
[[data.pagination.scroll]]
== Scroll
As explained in <<execution.pagination>>, the GraphQL Cursor Connection spec defines a
mechanism for pagination with `Connection`, `Edge`, and `PageInfo` schema types, while
GraphQL Java provides the equivalent Java type representations.
Spring for GraphQL provides built-in ``ConnectionAdapter`` implementations to adapt the
Spring Data pagination types `Window` and `Slice` transparently. You can configure that
as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
CursorStrategy<ScrollPosition> strategy = CursorStrategy.withEncoder(
new ScrollPositionCursorStrategy(),
CursorEncoder.base64()); // <1>
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
new WindowConnectionAdapter(strategy),
new SliceConnectionAdapter(strategy))); // <2>
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(..)
.typeVisitors(List.of(visitor)); // <3>
----
<1> Create strategy to convert `ScrollPosition` to a Base64 encoded cursor.
<2> Create type visitor to adapt `Window` and `Slice` returned from ``DataFetcher``s.
<3> Register the type visitor.
On the request side, a controller method can declare a
<<controllers.schema-mapping.subrange,ScrollSubrange>> method argument to paginate forward
or backward. For this to work, you must declare a <<execution.pagination.cursor.strategy>>
supports `ScrollPosition` as a bean.
The <<boot-starter>> declares a `CursorStrategy<ScrollPosition>` bean, and registers the
`ConnectionFieldTypeVisitor` as shown above if Spring Data is on the classpath.
[[data.pagination.scroll.keyset]]
== Keyset Position
For `KeysetScrollPosition`, the cursor needs to be created from a keyset, which is
essentially a `Map` of key-value pairs. To decide how to create a cursor from a keyset,
you can configure `ScrollPositionCursorStrategy` with `CursorStrategy<Map<String, Object>>`.
By default, `JsonKeysetCursorStrategy` writes the keyset `Map` to JSON. That works for
simple like String, Boolean, Integer, and Double, but others cannot be restored back to the
same type without target type information. The Jackson library has a default typing feature
that can include type information in the JSON. To use it safely you must specify a list of
allowed types. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Map.class)
.allowIfSubType(ZonedDateTime.class)
.build();
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL);
----
You can then create `JsonKeysetCursorStrategy`:
[source,java,indent=0,subs="verbatim,quotes"]
----
ObjectMapper mapper = ... ;
CodecConfigurer configurer = ServerCodecConfigurer.create();
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));
JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer);
----
By default, if `JsonKeysetCursorStrategy` is created without a `CodecConfigurer` and the
Jackson library is on the classpath, customizations like the above are applied for
`Date`, `Calendar`, and any type from `java.time`.
[[data.pagination.sort]]
== Sort
Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL arguments.
`AbstractSortStrategy` implements the contract with abstract methods to extract the sort
direction and properties. To enable support for `Sort` as a controller method argument,
you need to declare a `SortStrategy` bean.

View File

@@ -1,56 +0,0 @@
[[graalvm]]
= GraalVM Native support
Spring Framework 6.0 introduced the support infrastructure for compiling Spring applications to https://www.graalvm.org/22.3/reference-manual/native-image/[GraalVM Native images].
If you are not familiar with GraalVM in general, how this differs from applications deployed on the JVM and what it means for Spring application,
please refer to the dedicated {spring-boot-ref-docs}/native-image.html#native-image[Spring Boot 3.0 GraalVM Native Image support documentation].
Spring Boot also documents the https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-with-GraalVM[know limitations with the GraalVM support in Spring].
[[graalvm.graphql-java]]
== GraphQL Java metadata
Since the {spring-boot-ref-docs}/native-image.html#native-image.introducing-graalvm-native-images.key-differences-with-jvm-deployments[static analysis of your application is done at build time],
GraalVM might need extra hints if your application is looking up static resources, performing reflection or creating JDK proxies at runtime.
GraphQL Java is performing three tasks at runtime that Native Images are sensible to:
1. Loading resource bundles for message internationalization
2. Some reflection on internal types for schema inspection
3. Reflection on Java types that your application registers with the schema. This happens for example when GraphQL Java is fetching properties from application types
The first two items are handled via reachability metadata that has been contributed by the Spring team to
https://github.com/oracle/graalvm-reachability-metadata/tree/master/metadata/com.graphql-java/graphql-java[the GraalVM reachability metadata repository].
This metadata is automatically fetched by the native compilation tool when building an application that depends on GraphQL Java.
This doesn't cover our third item in the list, as those types are provided by the application itself and must be discovered by another mean.
[[graalvm.server]]
== Native Server applications support
In typical Spring for GraphQL applications, Java types tied to the GraphQL schema are exposed in `@Controller` method signatures
as parameters or return types. During the {spring-framework-ref-docs}/core/aot.html[Ahead Of Time processing phase] of the build,
Spring or GraphQL will use its `o.s.g.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor` to discover
the relevant types and register reachability metadata accordingly.
This is all done automatically for you if you are building a Spring Boot application with GraalVM support.
If your application is "manually" registering data fetchers, some types are not discoverable as a result.
You should then register them with Spring Framework's `@RegisterReflectionForBinding`:
include::code:GraphQlConfiguration[]
<1> This application declares a `RuntimeWiringConfigurer` that "manually" adds a `DataFetcher`
<2> Through this `DataFetcher`, the `BookRepository` will expose a `Book` type
<3> `@RegisterReflectionForBinding` will register the relevant hints for the `Book` type and all types exposed as fields
[[graalvm.client]]
== Client support
The `GraphQlClient` is not necessarily present as a bean in the application context and it does not expose the Java types used in the schema in method signatures.
The `AotProcessor` strategy described in the section above cannot be used as a result.
For client support, Spring for GraphQL embeds the {github-main-branch}/spring-graphql/src/main/resources/META-INF/native-image/org.springframework.graphql/spring-graphql[relevant reachability metadata for the client infrastructure].
When it comes to Java types used by the application, applications should use a similar strategy as "manual" data fetchers using `@RegisterReflectionForBinding`:
include::code:ProjectService[]
<1> In a Native image, we need to ensure that reflection can be performed on `Project` at runtime
<2> `@RegisterReflectionForBinding` will register the relevant hints for the `Project` type and all types exposed as fields

View File

@@ -1,38 +0,0 @@
[[graphiql]]
= GraphiQL
https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme[GraphiQL] is a graphical interactive in-browser GraphQL IDE.
It is very popular amongst developers as it makes it easy to explore and interactively develop GraphQL APIs.
During development, a stock GraphiQL integration is often enough to help developers work on an API.
In production, applications can require a custom GraphiQL build, that ships with a company logo or specific authentication support.
Spring for GraphQL ships with https://github.com/spring-projects/spring-graphql/blob/main/spring-graphql/src/main/resources/graphiql/index.html[a stock GraphiQL `index.html` page] that uses static resources hosted on the unpkg.com CDN.
Spring Boot applications can easily {spring-boot-ref-docs}/web.html#web.graphql.graphiql[enable this page with a configuration property].
Your application may need a custom GraphiQL build if it requires a setup that doesn't rely on a CDN, or if you wish to customize the user interface.
This can be done in two steps:
1. Configure and compile a GraphiQL build
2. Expose the built GraphiQL instance through the Spring web infrastructure
[[graphiql.custombuild]]
== Creating a custom GraphiQL build
This part is generally outside of the scope of this documentation, as there are several options for custom builds.
You will find more information in the https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme[official GraphiQL documentation].
You can choose to copy the build result directly in your application resources.
Alternatively, you can integrate the JavaScript build in your project as a separate module by leveraging Node.js https://github.com/node-gradle/gradle-node-plugin[Gradle] or https://github.com/eirslett/frontend-maven-plugin[Maven] build plugins.
[[graphiql.configuration]]
== Exposing a GraphiQL instance
Once a GraphiQL build is available on the classpath, you can expose it as an endpoint with the {spring-framework-ref-docs}/web/webmvc-functional.html#webmvc-fn-router-functions[functional web frameworks].
include::code:GraphiQlConfiguration[]
<1> Load the GraphiQL page from the classpath (here, we are using the version shipped with Spring for GraphQL)
<2> Configure a web handler for processing HTTP requests; you can implement a custom `HandlerFunction` depending on your use case
<3> Finally, map the handler to a specific HTTP endpoint
<4> Expose this new route through a `RouterFunction` bean
You might also need to configure your application to {spring-boot-ref-docs}/web.html#web.servlet.spring-mvc.static-content[serve the relevant static resources].

View File

@@ -1,69 +0,0 @@
[[observability]]
= Observability
https://micrometer.io/docs/observation[Observability support with Micrometer] is directly instrumented in Spring for GraphQL.
This enables both metrics and traces for GraphQL requests and "non-trivial" data fetching operations.
Because the GraphQL engine operates on top of a transport layer, you should also {spring-framework-ref-docs}/integration/observability.html[expect observations from the transport], if supported in Spring Framework.
Observations are only published if an `ObservationRegistry` is configured in the application.
You can learn more about {spring-boot-ref-docs}/actuator.html#actuator.metrics[configuring the observability infrastructure in Spring Boot].
If you would like to customize the metadata produced with the GraphQL observations, you can {spring-framework-ref-docs}/integration/observability.html#observability.config.conventions[configure a custom convention on the instrumentation directly].
If your application is using Spring Boot, contributing the custom convention as a bean is the preferred way.
[[observability.server.request]]
== Server Requests instrumentation
GraphQL Server Requests observations are created with the name `"graphql.request"` for traditional and Reactive applications and above all supported transports.
This instrumentation assumes that any parent observation must be set as the current one on the GraphQL context with the well-known `"micrometer.observation"` key.
For trace propagation across network boundaries, a separate instrumentation at the transport level must be in charge.
In the case of HTTP, Spring Framework {spring-framework-ref-docs}/integration/observability.html#observability.http-server[has dedicated instrumentation that takes care of trace propagation].
Applications need to configure the `org.springframework.graphql.observation.GraphQlObservationInstrumentation` instrumentation in their application.
It is using the `org.springframework.graphql.observation.DefaultExecutionRequestObservationConvention` by default, backed by the `ExecutionRequestObservationContext`.
By default, the following KeyValues are created:
.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`graphql.operation` _(required)_|GraphQL Operation name.
|`graphql.outcome` _(required)_|Outcome of the GraphQL request.
|===
The `graphql.operation` KeyValue will use the custom name of the provided query, or http://spec.graphql.org/draft/#sec-Language.Operations[the standard name for the operation] if none (`"query"`, `"mutation"` or `"subscription"`).
The `graphql.outcome` KeyValue will be `"SUCCESS"` if a valid GraphQL response has been sent, `"REQUEST_ERROR"` if the request could not be parsed, or `"INTERNAL_ERROR"` if no valid GraphQL response could be produced.
.High cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`graphql.execution.id` _(required)_|`graphql.execution.ExecutionId` of the GraphQL request.
|===
[[observability.server.datafetcher]]
== DataFetcher instrumentation
GraphQL DataFetcher observations are created with the name `"graphql.datafetcher"`, only for data fetching operations that are considered as "non trivial" (property fetching on a Java object is a trivial operation).
Applications need to configure the `org.springframework.graphql.observation.GraphQlObservationInstrumentation` instrumentation in their application.
It is using the `org.springframework.graphql.observation.DefaultDataFetcherObservationConvention` by default, backed by the `DataFetcherObservationContext`.
By default, the following KeyValues are created:
.Low cardinality Keys
[cols="a,a"]
|===
|Name | Description
|`graphql.error.type` _(required)_|Class name of the data fetching error
|`graphql.field.name` _(required)_|Name of the field being fetched.
|`graphql.outcome` _(required)_|Outcome of the GraphQL data fetching operation, "SUCCESS" or "ERROR".
|===
.High cardinality Keys
|===
|Name | Description
|`graphql.field.path` _(required)_|Path to the field being fetched (for example, "/bookById").
|===

View File

@@ -1,718 +0,0 @@
[[execution]]
= Request Execution
`ExecutionGraphQlService` is the main Spring abstraction to call GraphQL Java to execute
requests. Underlying transports, such as the <<server.transports.http>>, delegate to
`ExecutionGraphQlService` to handle requests.
The main implementation, `DefaultExecutionGraphQlService`, is configured with a
`GraphQlSource` for access to the `graphql.GraphQL` instance to invoke.
[[execution.graphqlsource]]
== `GraphQLSource`
`GraphQlSource` is a contract to expose the `graphql.GraphQL` instance to use that also
includes a builder API to build that instance. The default builder is available via
`GraphQlSource.schemaResourceBuilder()`.
The <<boot-starter>> creates an instance of this builder and further initializes it
to <<execution.graphqlsource.schema-resources, load schema files>> from a configurable location,
to {spring-boot-ref-docs}/application-properties.html#appendix.application-properties.web[expose properties]
to apply to `GraphQlSource.Builder`, to detect
<<execution.graphqlsource.runtimewiring-configurer>> beans,
https://www.graphql-java.com/documentation/instrumentation[Instrumentation] beans for
{spring-boot-ref-docs}/actuator.html#actuator.metrics.supported.spring-graphql[GraphQL metrics],
and `DataFetcherExceptionResolver` and `SubscriptionExceptionResolver` beans for
<<execution.exceptions, exception resolution>>. For further customizations, you can also
declare a `GraphQlSourceBuilderCustomizer` bean, for example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration(proxyBeanMethods = false)
class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) ->
builder.configureGraphQl(graphQlBuilder ->
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
}
}
----
[[execution.graphqlsource.schema-resources]]
=== Schema Resources
`GraphQlSource.Builder` can be configured with one or more `Resource` instances to be
parsed and merged together. That means schema files can be loaded from just about any
location.
By default, the Boot starter
{spring-boot-ref-docs}/web.html#web.graphql.schema[looks for schema files] with extensions
".graphqls" or ".gqls" under the location `classpath:graphql/**`, which is typically
`src/main/resources/graphql`. You can also use a file system location, or any location
supported by the Spring `Resource` hierarchy, including a custom implementation that
loads schema files from remote locations, from storage, or from memory.
TIP: Use `classpath*:graphql/**/` to find schema files across multiple classpath
locations, e.g. across multiple modules.
[[execution.graphqlsource.schema-creation]]
=== Schema Creation
By default, `GraphQlSource.Builder` uses the GraphQL Java `SchemaGenerator` to create the
`graphql.schema.GraphQLSchema`. This works for typical use, but if you need to use a
different generator, e.g. for federation, you can register a `schemaFactory` callback:
[source,java,indent=0,subs="verbatim,quotes"]
----
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
----
The <<execution.graphqlsource, GraphQlSource section>> explains how to configure that with Spring Boot.
For an example with Apollo Federation, see
https://github.com/apollographql/federation-jvm-spring-example[federation-jvm-spring-example].
[[execution.graphqlsource.runtimewiring-configurer]]
=== `RuntimeWiringConfigurer`
You can use `RuntimeWiringConfigurer` to register:
- Custom scalar types.
- <<execution.graphqlsource.directives>> handling code.
- Default <<execution.graphqlsource.default-type-resolver>> for interface and union types.
- `DataFetcher` for a field although applications will typically use <<controllers>>, and
those are detected and registered as ``DataFetcher``s by `AnnotatedControllerConfigurer`,
which is a `RuntimeWiringConfigurer`. The <<boot-starter>> automatically registers
`AnnotatedControllerConfigurer`.
NOTE: GraphQL Java, server applications use Jackson only for serialization to and from maps of data.
Client input is parsed into a map. Server output is assembled into a map based on the field selection set.
This means you can't rely on Jackson serialization/deserialization annotations.
Instead, you can use https://www.graphql-java.com/documentation/scalars/[custom scalar types].
The <<boot-starter>> detects beans of type `RuntimeWiringConfigurer` and
registers them in the `GraphQlSource.Builder`. That means in most cases, you'll' have
something like the following in your configuration:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {
GraphQLScalarType scalarType = ... ;
SchemaDirectiveWiring directiveWiring = ... ;
DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();
return wiringBuilder -> wiringBuilder
.scalar(scalarType)
.directiveWiring(directiveWiring)
.type("Query", builder -> builder.dataFetcher("book", dataFetcher));
}
}
----
If you need to add a `WiringFactory`, e.g. to make registrations that take into account
schema definitions, implement the alternative `configure` method that accepts both the
`RuntimeWiring.Builder` and an output `List<WiringFactory>`. This allows you to add any
number of factories that are then invoked in sequence.
[[execution.graphqlsource.default-type-resolver]]
=== `TypeResolver`
`GraphQlSource.Builder` registers `ClassNameTypeResolver` as the default `TypeResolver`
to use for GraphQL Interfaces and Unions that don't already have such a registration
through a <<execution.graphqlsource.runtimewiring-configurer>>. The purpose of
a `TypeResolver` in GraphQL Java is to determine the GraphQL Object type for values
returned from the `DataFetcher` for a GraphQL Interface or Union field.
`ClassNameTypeResolver` tries to match the simple class name of the value to a GraphQL
Object Type and if it is not successful, it also navigates its super types including
base classes and interfaces, looking for a match. `ClassNameTypeResolver` provides an
option to configure a name extracting function along with `Class` to GraphQL Object type
name mappings that should help to cover more corner cases:
[source,java,indent=0,subs="verbatim,quotes"]
----
GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
// Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);
----
The <<execution.graphqlsource, GraphQlSource section>> explains how to configure that with Spring Boot.
[[execution.graphqlsource.directives]]
=== Directives
The GraphQL language supports directives that "describe alternate runtime execution and
type validation behavior in a GraphQL document". Directives are similar to annotations in
Java but declared on types, fields, fragments and operations in a GraphQL document.
GraphQL Java provides the `SchemaDirectiveWiring` contract to help applications detect
and handle directives. For more details, see
{graphql-java-docs}/sdl-directives/[Schema Directives] in the
GraphQL Java documentation.
In Spring GraphQL you can register a `SchemaDirectiveWiring` through a
<<execution.graphqlsource.runtimewiring-configurer>>. The <<boot-starter>> detects
such beans, so you might have something like:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
----
TIP: For an example of directives support check out the
https://github.com/graphql-java/graphql-java-extended-validation[Extended Validation for Graphql Java]
library.
[[execution.graphqlsource.schema-transformation]]
=== Schema Transformation
You can register a `graphql.schema.GraphQLTypeVisitor` via
`builder.schemaResources(..).typeVisitorsToTransformSchema(..)` if you want to traverse
and transform the schema after it is created, and make changes to the schema. Keep in mind
that this is more expensive than <<execution.graphqlsource.schema-traversal>> so generally
prefer traversal to transformation unless you need to make schema changes.
[[execution.graphqlsource.schema-traversal]]
=== Schema Traversal
You can register a `graphql.schema.GraphQLTypeVisitor` via
`builder.schemaResources(..).typeVisitors(..)` if you want to traverse the schema after
it is created, and possibly apply changes to the `GraphQLCodeRegistry`. Keep in mind,
however, that such a visitor cannot change the schema. See
<<execution.graphqlsource.schema-transformation>>, if you need to make changes to the schema.
[[execution.graphqlsource.schema-mapping-inspection]]
=== Schema Mapping Inspection
If a query, mutation, or subscription operation does not have a `DataFetcher`, it won't
return any data, and won't do anything useful. Likewise, fields on schema types returned
by an operation that are covered neither explicitly through a `DataFetcher`
registration, nor implicitly by the default `PropertyDataFetcher`, which looks for a
matching Java object property, will always be `null`.
GraphQL Java does not perform checks to ensure every schema field is covered, and that
can result in gaps that might not be discovered depending on test coverage. At runtime
you may get a "silent" `null`, or an error if the field is not nullable. As a lower level
library, GraphQL Java simply does not know enough about `DataFetcher` implementations and
their return types, and therefore can't compare schema type structure against Java object
structure.
Spring for GraphQL defines the `SelfDescribingDataFetcher` interface to allow a
`DataFetcher` to expose return type information. All Spring `DataFetcher` implementations
implement this interface. That includes those for <<controllers>>, and those for
<<data.querydsl>> and <<data.querybyexample>> Spring Data repositories. For annotated
controllers, the return type is derived from the declared return type on a
`@SchemaMapping` method.
On startup, Spring for GraphQL can inspect schema fields, `DataFetcher` registrations,
and the properties of Java objects returned from `DataFetcher` implementations to check
if all schema fields are covered either by an explicitly registered `DataFetcher`, or
a matching Java object property. The inspection also performs a reverse check looking for
`DataFetcher` registrations against schema fields that don't exist.
To enable inspection of schema mappings:
[source,java,indent=0,subs="verbatim,quotes"]
----
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.inspectSchemaMappings(report -> {
logger.debug(report);
})
----
Below is an example report:
----
GraphQL schema inspection:
Unmapped fields: {Book=[title], Author[firstName, lastName]} // <1>
Unmapped registrations: {Book.reviews=BookController#reviews[1 args]} <2>
Skipped types: [BookOrAuthor] // <3>
----
<1> List of schema fields and their source types that are not mapped
<2> List of `DataFetcher` registrations on fields that don't exist
<3> List of schema types that are skipped, as explained next
There are limits to what schema field inspection can do, in particular when there is
insufficient Java type information. This is the case if an annotated controller method is
declared to return `java.lang.Object`, or if the return type has an unspecified generic
parameter such as `List<?>`, or if the `DataFetcher` does not implement
`SelfDescribingDataFetcher` and the return type is not even known. In such cases, the
Java object type structure remains unknown, and the schema type is listed as skipped in
the resulting report. For every skipped type, a DEBUG message is logged to indicate why
it was skipped.
Schema union types are always skipped because there is no way for a controller method to
declare such a return type in Java, and the Java type structure is unknown.
Schema interface types are supported only as far as fields declared directly, which are
compared against properties on the Java type declared by a `SelfDescribingDataFetcher`.
Additional fields on concrete implementations are not inspected. This could be improved
in a future release to also inspect schema `interface` implementation types and to try
to find a match among subtypes of the declared Java return type.
[[execution.graphqlsource.operation-caching]]
=== Operation Caching
GraphQL Java must _parse_ and _validate_ an operation before executing it. This may impact
performance significantly. To avoid the need to re-parse and validate, an application may
configure a `PreparsedDocumentProvider` that caches and reuses Document instances. The
{graphql-java-docs}/execution/#query-caching[GraphQL Java docs] provide more details on
query caching through a `PreparsedDocumentProvider`.
In Spring GraphQL you can register a `PreparsedDocumentProvider` through
`GraphQlSource.Builder#configureGraphQl`:
.
[source,java,indent=0,subs="verbatim,quotes"]
----
// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...
// Create provider
PreparsedDocumentProvider provider = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
----
The <<execution.graphqlsource, GraphQlSource section>> explains how to configure that with Spring Boot.
[[execution.reactive-datafetcher]]
== Reactive `DataFetcher`
The default `GraphQlSource` builder enables support for a `DataFetcher` to return `Mono`
or `Flux` which adapts those to a `CompletableFuture` where `Flux` values are aggregated
and turned into a List, unless the request is a GraphQL subscription request,
in which case the return value remains a Reactive Streams `Publisher` for streaming
GraphQL responses.
A reactive `DataFetcher` can rely on access to Reactor context propagated from the
transport layer, such as from a WebFlux request handling, see
<<execution.context.webflux, WebFlux Context>>.
[[execution.context]]
== Context Propagation
Spring for GraphQL provides support to transparently propagate context from the
<<server.transports.http>>, through GraphQL Java, and to `DataFetcher` and other components it
invokes. This includes both `ThreadLocal` context from the Spring MVC request handling
thread and Reactor `Context` from the WebFlux processing pipeline.
[[execution.context.webmvc]]
=== WebMvc
A `DataFetcher` and other components invoked by GraphQL Java may not always execute on
the same thread as the Spring MVC handler, for example if an asynchronous
<<server.interception, `WebGraphQlInterceptor`>> or `DataFetcher` switches to a
different thread.
Spring for GraphQL supports propagating `ThreadLocal` values from the Servlet container
thread to the thread a `DataFetcher` and other components invoked by GraphQL Java to
execute on. To do this, an application needs to implement
`io.micrometer.context.ThreadLocalAccessor` for a `ThreadLocal` values of interest:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> {
@Override
public Object key() {
return RequestAttributesAccessor.class.getName();
}
@Override
public RequestAttributes getValue() {
return RequestContextHolder.getRequestAttributes();
}
@Override
public void setValue(RequestAttributes attributes) {
RequestContextHolder.setRequestAttributes(attributes);
}
@Override
public void reset() {
RequestContextHolder.resetRequestAttributes();
}
}
----
You can register a `ThreadLocalAccessor` manually on startup with the global
`ContextRegistry` instance, which is accessible via
`io.micrometer.context.ContextRegistry#getInstance()`. You can also register it
automatically through the `java.util.ServiceLoader` mechanism.
[[execution.context.webflux]]
=== WebFlux
A <<execution.reactive-datafetcher>> can rely on access to Reactor context that
originates from the WebFlux request handling chain. This includes Reactor context
added by <<server.interception, WebGraphQlInterceptor>> components.
[[execution.exceptions]]
== Exceptions
In GraphQL Java, `DataFetcherExceptionHandler` decides how to represent exceptions from
data fetching in the "errors" section of the response. An application can register a
single handler only.
Spring for GraphQL registers a `DataFetcherExceptionHandler` that provides default
handling and enables the `DataFetcherExceptionResolver` contract. An application can
register any number of resolvers via <<execution.graphqlsource>> builder and those are in
order until one them resolves the `Exception` to a `List<graphql.GraphQLError>`.
The Spring Boot starter detects beans of this type.
`DataFetcherExceptionResolverAdapter` is a convenient base class with protected methods
`resolveToSingleError` and `resolveToMultipleErrors`.
The <<controllers>> programming model enables handling data fetching exceptions with
annotated exception handler methods with a flexible method signature, see
<<controllers.exception-handler>> for details.
A `GraphQLError` can be assigned to a category based on the GraphQL Java
`graphql.ErrorClassification`, or the Spring GraphQL `ErrorType`, which defines the following:
- `BAD_REQUEST`
- `UNAUTHORIZED`
- `FORBIDDEN`
- `NOT_FOUND`
- `INTERNAL_ERROR`
If an exception remains unresolved, by default it is categorized as an `INTERNAL_ERROR`
with a generic message that includes the category name and the `executionId` from
`DataFetchingEnvironment`. The message is intentionally opaque to avoid leaking
implementation details. Applications can use a `DataFetcherExceptionResolver` to customize
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.subscription]]
=== Subscription Exceptions
The `Publisher` for a subscription request may complete with an error signal in which case
the underlying transport (e.g. WebSocket) sends a final "error" type message with a list
of GraphQL errors.
`DataFetcherExceptionResolver` cannot resolve errors from a subscription `Publisher`,
since the data `DataFetcher` only creates the `Publisher` initially. After that, the
transport subscribes to the `Publisher` that may then complete with an error.
An application can register a `SubscriptionExceptionResolver` in order to resolve
exceptions from a subscription `Publisher` in order to resolve those to GraphQL errors
to send to the client.
[[execution.pagination]]
== Pagination
The GraphQL https://relay.dev/graphql/connections.htm[Cursor Connection specification]
defines a way to navigate large result sets by returning a subset of items at a time where
each item is paired with a cursor that clients can use to request more items before or
after the referenced item.
The specification calls the pattern _"Connections"_. A schema type with a name that ends
on Connection is a _Connection Type_ that represents a paginated result set. All `~Connection`
types contain an "edges" field where `~Edge` type pairs the actual item with a cursor, as
well as a "pageInfo" field with boolean flags to indicate if there are more items forward
and backward.
[[execution.pagination.types]]
=== Connection Types
`Connection` type definitions must be created for every type that needs pagination, adding
boilerplate and noise to the schema. Spring for GraphQL provides
`ConnectionTypeDefinitionConfigurer` to add these types on startup, if not already
present in the parsed schema files. That means in the schema you only need this:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
Query {
books(first:Int, after:String, last:Int, before:String): BookConnection
}
type Book {
id: ID!
title: String!
}
----
Note the spec-defined forward pagination arguments `first` and `after` that clients can use
to request the first N items after the given cursor, while `last` and `before` are backward
pagination arguments to request the last N items before the given cursor.
Next, configure `ConnectionTypeDefinitionConfigurer` as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
----
and the following type definitions will be transparently added to the schema:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
type BookConnection {
edges: [BookEdge]!
pageInfo: PageInfo!
}
type BookEdge {
node: Book!
cursor: String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
----
The <<boot-starter>> registers `ConnectionTypeDefinitionConfigurer` by default.
[[execution.pagination.adapters]]
=== `ConnectionAdapter`
Once <<execution.pagination.types>> are available in the schema, you also need
equivalent Java types. GraphQL Java provides those, including generic `Connection` and
`Edge`, as well as a `PageInfo`.
One option is to populate a `Connection` and return it from your controller method or
`DataFetcher`. However, this requires boilerplate code to create the `Connection`,
creating cursors, wrapping each item as an `Edge`, and creating the `PageInfo`.
Moreover, you may already have an underlying pagination mechanism such as when using
Spring Data repositories.
Spring for GraphQL defines the `ConnectionAdapter` contract to adapt a container of items
to `Connection`. Adapters are applied through a `DataFetcher` decorator that is in turn
installed through a `ConnectionFieldTypeVisitor`. You can configure it as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
ConnectionAdapter adapter = ... ;
GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) // <1>
GraphQlSource.schemaResourceBuilder()
.schemaResources(..)
.typeDefinitionConfigurer(..)
.typeVisitors(List.of(visitor)) // <2>
----
<1> Create type visitor with one or more ``ConnectionAdapter``s.
<2> Resister the type visitor.
There are <<data.pagination.scroll,built-in>> ``ConnectionAdapter``s for Spring Data's
`Window` and `Slice`. You can also create your own custom adapter. `ConnectionAdapter`
implementations rely on a <<execution.pagination.cursor.strategy>> to
create cursors for returned items. The same strategy is also used to support the
<<controllers.schema-mapping.subrange>> controller method argument that contains
pagination input.
[[execution.pagination.cursor.strategy]]
=== `CursorStrategy`
`CursorStrategy` is a contract to encode and decode a String cursor that refers to the
position of an item within a large result set. The cursor can be based on an index or
on a keyset.
A <<execution.pagination.adapters>> uses this to encode cursors for returned items.
<<controllers>> methods, <<data.querydsl>> repositories, and <<data.querybyexample>>
repositories use it to decode cursors from pagination requests, and create a `Subrange`.
`CursorEncoder` is a related contract that further encodes and decodes String cursors to
make them opaque to clients. `EncodingCursorStrategy` combines `CursorStrategy` with a
`CursorEncoder`. You can use `Base64CursorEncoder`, `NoOpEncoder` or create your own.
There is a <<data.pagination.scroll,built-in>> `CursorStrategy` for the Spring Data
`ScrollPosition`. The <<boot-starter>> registers a `CursorStrategy<ScrollPosition>` with
`Base64Encoder` when Spring Data is present.
[[execution.pagination.sort.strategy]]
=== Sort
There is no standard way to provide sort information in a GraphQL request. However,
pagination depends on a stable sort order. You can use a default order, or otherwise
expose input types and extract sort details from GraphQL arguments.
There is <<data.pagination.sort,built-in>> support for Spring Data's `Sort` as a controller
method argument. For this to work, you need to have a `SortStrategy` bean.
[[execution.batching]]
== Batch Loading
Given a `Book` and its `Author`, we can create one `DataFetcher` for a book and another
for its author. This allows selecting books with or without authors, but it means books
and authors aren't loaded together, which is especially inefficient when querying multiple
books as the author for each book is loaded individually. This is known as the N+1 select
problem.
[[execution.batching.dataloader]]
=== `DataLoader`
GraphQL Java provides a `DataLoader` mechanism for batch loading of related entities.
You can find the full details in the
{graphql-java-docs}/batching/[GraphQL Java docs]. Below is a
summary of how it works:
1. Register ``DataLoader``'s in the `DataLoaderRegistry` that can load entities, given unique keys.
2. ``DataFetcher``'s can access ``DataLoader``'s and use them to load entities by id.
3. A `DataLoader` defers loading by returning a future so it can be done in a batch.
4. ``DataLoader``'s maintain a per request cache of loaded entities that can further
improve efficiency.
[[execution.batching.batch-loader-registry]]
=== `BatchLoaderRegistry`
The complete batching loading mechanism in GraphQL Java requires implementing one of
several `BatchLoader` interface, then wrapping and registering those as ``DataLoader``s
with a name in the `DataLoaderRegistry`.
The API in Spring GraphQL is slightly different. For registration, there is only one,
central `BatchLoaderRegistry` exposing factory methods and a builder to create and
register any number of batch loading functions:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class MyConfig {
public MyConfig(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Mono<Map<Long, Author>
});
// more registrations ...
}
}
----
The <<boot-starter>> declares a `BatchLoaderRegistry` bean that you can inject into
your configuration, as shown above, or into any component such as a controller in order
register batch loading functions. In turn the `BatchLoaderRegistry` is injected into
`DefaultExecutionGraphQlService` where it ensures `DataLoader` registrations per request.
By default, the `DataLoader` name is based on the class name of the target entity.
This allows an `@SchemaMapping` method to declare a
<<controllers.schema-mapping.data-loader,DataLoader argument>> with a generic type, and
without the need for specifying a name. The name, however, can be customized through the
`BatchLoaderRegistry` builder, if necessary, along with other `DataLoaderOptions`.
To configure default `DataLoaderOptions` globally, to use as a starting point for any
registration, you can override Boot's `BatchLoaderRegistry` bean and use the constructor
for `DefaultBatchLoaderRegistry` that accepts `Supplier<DataLoaderOptions>`.
For many cases, when loading related entities, you can use
<<controllers.batch-mapping,@BatchMapping>> controller methods, which are a shortcut
for and replace the need to use `BatchLoaderRegistry` and `DataLoader` directly.
`BatchLoaderRegistry` provides other important benefits too. It supports access to
the same `GraphQLContext` from batch loading functions and from `@BatchMapping` methods,
as well as ensures <<execution.context>> to them. This is why applications are expected
to use it. It is possible to perform your own `DataLoader` registrations directly but
such registrations would forgo the above benefits.
[[execution.batching.testing]]
=== Testing Batch Loading
Start by having `BatchLoaderRegistry` perform registrations on a `DataLoaderRegistry`:
[source,java,indent=0,subs="verbatim,quotes"]
----
BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
// perform registrations...
DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);
----
Now you can access and test individual ``DataLoader``'s as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
loader.load(1L);
loader.loadMany(Arrays.asList(2L, 3L));
List<Book> books = loader.dispatchAndJoin(); // actual loading
assertThat(books).hasSize(3);
assertThat(books.get(0).getName()).isEqualTo("...");
// ...
----

View File

@@ -1,462 +0,0 @@
[[testing]]
= Testing
Spring for GraphQL provides dedicated support for testing GraphQL requests over HTTP,
WebSocket, and RSocket, as well as for testing directly against a server.
To make use of this, add `spring-graphql-test` to your build:
[source,groovy,indent=0,subs="verbatim,quotes,attributes",role="primary"]
.Gradle
----
dependencies {
// ...
testImplementation 'org.springframework.graphql:spring-graphql-test:{spring-graphql-version}'
}
----
[source,xml,indent=0,subs="verbatim,quotes,attributes",role="secondary"]
.Maven
----
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<version>{spring-graphql-version}</version>
<scope>test</scope>
</dependency>
</dependencies>
----
[[testing.graphqltester]]
== `GraphQlTester`
`GraphQlTester` is a contract that declares a common workflow for testing GraphQL
requests that is independent of the underlying transport. That means requests are tested
with the same API no matter what the underlying transport, and anything transport
specific is configured at build time.
To create a `GraphQlTester` that performs requests through a client, you need one of the
following extensions:
- <<testing.httpgraphqltester, HttpGraphQlTester>>
- <<testing.websocketgraphqltester, WebSocketGraphQlTester>>
- <<testing.rsocketgraphqltester, RSocketGraphQlTester>>
To create a `GraphQlTester` that performs tests on the server side, without a client:
- <<testing.graphqlservicetester, ExecutionGraphQlServiceTester>>
- <<testing.webgraphqltester, WebGraphQlServiceTester>>
Each defines a `Builder` with options relevant to the transport. All builders extend
from a common, base GraphQlTester <<testing.graphqltester.builder, `Builder`>> with
options relevant to all extensions.
[[testing.httpgraphqltester]]
=== HTTP
`HttpGraphQlTester` uses
{spring-framework-ref-docs}/testing/webtestclient.html[WebTestClient] to execute
GraphQL requests over HTTP, with or without a live server, depending on how
`WebTestClient` is configured.
To test in Spring WebFlux, without a live server, point to your Spring configuration
that declares the GraphQL HTTP endpoint:
[source,java,indent=0,subs="verbatim,quotes"]
----
ApplicationContext context = ... ;
WebTestClient client =
WebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
----
To test in Spring MVC, without a live server, do the same using `MockMvcWebTestClient`:
[source,java,indent=0,subs="verbatim,quotes"]
----
ApplicationContext context = ... ;
WebTestClient client =
MockMvcWebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
----
Or to test against a live server running on a port:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebTestClient client =
WebTestClient.bindToServer()
.baseUrl("http://localhost:8080/graphql")
.build();
HttpGraphQlTester tester = HttpGraphQlTester.create(client);
----
Once `HttpGraphQlTester` is created, you can begin to
<<testing.requests, execute requests>> using the same API, independent of the underlying
transport. If you need to change any transport specific details, use `mutate()` on an
existing `HttpSocketGraphQlTester` to create a new instance with customized settings:
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use tester...
HttpGraphQlTester anotherTester = tester.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherTester...
----
[[testing.websocketgraphqltester]]
=== WebSocket
`WebSocketGraphQlTester` executes GraphQL requests over a shared WebSocket connection.
It is built using the
{spring-framework-ref-docs}/web/webflux-websocket.html#webflux-websocket-client[WebSocketClient]
from Spring WebFlux and you can create it as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();
----
`WebSocketGraphQlTester` is connection oriented and multiplexed. Each instance establishes
its own single, shared connection for all requests. Typically, you'll want to use a single
instance only per server.
Once `WebSocketGraphQlTester` is created, you can begin to
<<testing.requests, execute requests>> using the same API, independent of the underlying
transport. If you need to change any transport specific details, use `mutate()` on an
existing `WebSocketGraphQlTester` to create a new instance with customized settings:
[source,java,indent=0,subs="verbatim,quotes"]
----
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
// Use tester...
WebSocketGraphQlTester anotherTester = tester.mutate()
.headers(headers -> headers.setBasicAuth("peter", "..."))
.build();
// Use anotherTester...
----
`WebSocketGraphQlTester` provides a `stop()` method that you can use to have the WebSocket
connection closed, e.g. after a test runs.
[[testing.rsocketgraphqltester]]
=== RSocket
`RSocketGraphQlTester` uses `RSocketRequester` from spring-messaging to execute GraphQL
requests over RSocket:
[source,java,indent=0,subs="verbatim,quotes"]
----
URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);
RSocketGraphQlTester client = RSocketGraphQlTester.builder()
.clientTransport(transport)
.build();
----
`RSocketGraphQlTester` is connection oriented and multiplexed. Each instance establishes
its own single, shared session for all requests. Typically, you'll want to use a single
instance only per server. You can use the `stop()` method on the tester to close the
session explicitly.
Once `RSocketGraphQlTester` is created, you can begin to
<<testing.requests, execute requests>> using the same API, independent of the underlying
transport.
[[testing.graphqlservicetester]]
=== `GraphQlService`
Many times it's enough to test GraphQL requests on the server side, without the use of a
client to send requests over a transport protocol. To test directly against a
`ExecutionGraphQlService`, use the `ExecutionGraphQlServiceTester` extension:
[source,java,indent=0,subs="verbatim,quotes"]
----
GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);
----
Once `ExecutionGraphQlServiceTester` is created, you can begin to
<<testing.requests, execute requests>> using the same API, independent of the underlying
transport.
`ExecutionGraphQlServiceTester.Builder` provides an option to customize `ExecutionInput` details:
[source,java,indent=0,subs="verbatim,quotes"]
----
GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.builder(service)
.configureExecutionInput((executionInput, builder) -> builder.executionId(id).build())
.build();
----
[[testing.webgraphqltester]]
=== `WebGraphQlHandler`
The <<testing.graphqlservicetester>> extension lets you test on the server side, without
a client. However, in some cases it's useful to involve server side transport
handling with given mock transport input.
The `WebGraphQlTester` extension lets you processes request through the
`WebGraphQlInterceptor` chain before handing off to `ExecutionGraphQlService` for
request execution:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.create(handler);
----
The builder for this extension allows you to define HTTP request details:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.builder(handler)
.headers(headers -> headers.setBasicAuth("joe", "..."))
.build();
----
Once `WebGraphQlServiceTester` is created, you can begin to
<<testing.requests, execute requests>> using the same API, independent of the underlying
transport.
[[testing.graphqltester.builder]]
=== Builder
`GraphQlTester` defines a parent `Builder` with common configuration options for the
builders of all extensions. It lets you configure the following:
- `errorFilter` - a predicate to suppress expected errors, so you can inspect the data
of the response.
- `documentSource` - a strategy for loading the document for a request from a file on
the classpath or from anywhere else.
- `responseTimeout` - how long to wait for request execution to complete before timing
out.
[[testing.requests]]
== Requests
Once you have a `GraphQlTester`, you can begin to test requests. The below executes a
query for a project and uses https://github.com/json-path/JsonPath[JsonPath] to extract
project release versions from the response:
[source,java,indent=0,subs="verbatim,quotes"]
----
String document = "{" +
" project(slug:\"spring-framework\") {" +
" releases {" +
" version" +
" }"+
" }" +
"}";
graphQlTester.document(document)
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
----
The JsonPath is relative to the "data" section of the response.
You can also create document files with extensions `.graphql` or `.gql` under
`"graphql-test/"` on the classpath and refer to them by file name.
For example, given a file called `projectReleases.graphql` in
`src/main/resources/graphql-test`, with content:
[source,graphql,indent=0,subs="verbatim,quotes"]
----
query projectReleases($slug: ID!) {
project(slug: $slug) {
releases {
version
}
}
}
----
You can then use:
[source,java,indent=0,subs="verbatim,quotes"]
----
graphQlTester.documentName("projectReleases") <1>
.variable("slug", "spring-framework") <2>
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
----
<1> Refer to the document in the file named "project".
<2> Set the `slug` variable.
[TIP]
====
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
====
If a request does not have any response data, e.g. mutation, use `executeAndVerify`
instead of `execute` to verify there are no errors in the response:
[source,java,indent=0,subs="verbatim,quotes"]
----
graphQlTester.query(query).executeAndVerify();
----
See <<testing.errors>> for more details on error handling.
[[testing.requests.nestedPaths]]
=== Nested Paths
By default, paths are relative to the "data" section of the GraphQL response. You can also
nest down to a path, and inspect multiple paths relative to it as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
graphQlTester.document(document)
.execute()
.path("project", project -> project // <1>
.path("name").entity(String.class).isEqualTo("spring-framework")
.path("releases[*].version").entityList(String.class).hasSizeGreaterThan(1));
----
<1> Use a callback to inspect paths relative to "project".
[[testing.subscriptions]]
== Subscriptions
To test subscriptions, call `executeSubscription` instead of `execute` to obtain a stream
of responses and then use `StepVerifier` from Project Reactor to inspect the stream:
[source,java,indent=0,subs="verbatim,quotes"]
----
Flux<String> greetingFlux = tester.document("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class); // decode at JSONPath
StepVerifier.create(greetingFlux)
.expectNext("Hi")
.expectNext("Bonjour")
.expectNext("Hola")
.verifyComplete();
----
Subscriptions are supported only with <<testing.websocketgraphqltester,
WebSocketGraphQlTester>>, or with the server side
<<testing.graphqlservicetester>> and <<testing.webgraphqltester>> extensions.
[[testing.errors]]
== Errors
When you use `verify()`, any errors under the "errors" key in the response will cause
an assertion failure. To suppress a specific error, use the error filter before
`verify()`:
[source,java,indent=0,subs="verbatim,quotes"]
----
graphQlTester.query(query)
.execute()
.errors()
.filter(error -> ...)
.verify()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
----
You can register an error filter at the builder level, to apply to all tests:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client)
.errorFilter(error -> ...)
.build();
----
If you want to verify that an error does exist, and in contrast to `filter`, throw an
assertion error if it doesn't, then use `exepect` instead:
[source,java,indent=0,subs="verbatim,quotes"]
----
graphQlTester.query(query)
.execute()
.errors()
.expect(error -> ...)
.verify()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
----
You can also inspect all errors through a `Consumer`, and doing so also marks them as
filtered, so you can then also inspect the data in the response:
[source,java,indent=0,subs="verbatim,quotes"]
----
graphQlTester.query(query)
.execute()
.errors()
.satisfy(errors -> {
// ...
});
----

View File

@@ -1,164 +0,0 @@
[[server.transports]]
= Server Transports
Spring for GraphQL supports server handling of GraphQL requests over HTTP, WebSocket, and
RSocket.
[[server.transports.http]]
== HTTP
`GraphQlHttpHandler` handles GraphQL over HTTP requests and delegates to the
<<server.interception>> chain for request execution. There are two variants, one for
Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have
equivalent functionality, but rely on blocking vs non-blocking I/O respectively for
writing the HTTP response.
Requests must use HTTP POST with `"application/json"` as content type and GraphQL request details
included as JSON in the request body, as defined in the proposed
https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md[GraphQL over HTTP] specification.
Once the JSON body has been successfully decoded, the HTTP response status is always 200 (OK),
and any errors from GraphQL request execution appear in the "errors" section of the GraphQL response.
The default and preferred choice of media type is `"application/graphql-response+json"`, but `"application/json"`
is also supported, as described in the specification.
`GraphQlHttpHandler` can be exposed as an HTTP endpoint by declaring a `RouterFunction`
bean and using the `RouterFunctions` from Spring MVC or WebFlux to create the route. The
<<boot-starter>> does this, see the
{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints] section for
details, or check `GraphQlWebMvcAutoConfiguration` or `GraphQlWebFluxAutoConfiguration`
it contains, for the actual config.
The 1.0.x branch of this repository contains a Spring MVC
{github-10x-branch}/samples/webmvc-http[HTTP sample] application.
[[server.transports.http.fileupload]]
=== File Upload
As a protocol GraphQL focuses on the exchange of textual data. This doesn't include binary
data such as images, but there is a separate, informal
https://github.com/jaydenseric/graphql-multipart-request-spec[graphql-multipart-request-spec]
that allows file uploads with GraphQL over HTTP.
Spring for GraphQL does not support the `graphql-multipart-request-spec` directly.
While the spec does provide the benefit of a unified GraphQL API, the actual experince has
led to a number of issues, and best practice recommendations have evolved, see
https://www.apollographql.com/blog/backend/file-uploads/file-upload-best-practices/[Apollo Server File Upload Best Practices]
for a more detailed discussion.
If you would like to use `graphql-multipart-request-spec` in your application, you can
do so through the library
https://github.com/nkonev/multipart-spring-graphql[multipart-spring-graphql].
[[server.transports.websocket]]
== WebSocket
`GraphQlWebSocketHandler` handles GraphQL over WebSocket requests based on the
https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md[protocol] defined in the
https://github.com/enisdenjo/graphql-ws[graphql-ws] library. The main reason to use
GraphQL over WebSocket is subscriptions which allow sending a stream of GraphQL
responses, but it can also be used for regular queries with a single response.
The handler delegates every request to the <<server.interception>> chain for further
request execution.
[TIP]
.GraphQL Over WebSocket Protocols
====
There are two such protocols, one in the
https://github.com/apollographql/subscriptions-transport-ws[subscriptions-transport-ws]
library and another in the
https://github.com/enisdenjo/graphql-ws[graphql-ws] library. The former is not active and
succeeded by the latter. Read this
https://the-guild.dev/blog/graphql-over-websockets[blog post] for the history.
====
There are two variants of `GraphQlWebSocketHandler`, one for Spring MVC and one for
Spring WebFlux. Both handle requests asynchronously and have equivalent functionality.
The WebFlux handler also uses non-blocking I/O and back pressure to stream messages,
which works well since in GraphQL Java a subscription response is a Reactive Streams
`Publisher`.
The `graphql-ws` project lists a number of
https://github.com/enisdenjo/graphql-ws#recipes[recipes] for client use.
`GraphQlWebSocketHandler` can be exposed as a WebSocket endpoint by declaring a
`SimpleUrlHandlerMapping` bean and using it to map the handler to a URL path. By default,
the <<boot-starter>> does not expose a GraphQL over WebSocket endpoint, but it's easy to
enable it by adding a property for the endpoint path. Please, see the
{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints]
section for details, or check the `GraphQlWebMvcAutoConfiguration` or the
`GraphQlWebFluxAutoConfiguration` for the actual Boot starter config.
The 1.0.x branch of this repository contains a WebFlux
{github-10x-branch}/samples/webflux-websocket[WebSocket sample] application.
[[server.transports.rsocket]]
== RSocket
`GraphQlRSocketHandler` handles GraphQL over RSocket requests. Queries and mutations are
expected and handled as an RSocket `request-response` interaction while subscriptions are
handled as `request-stream`.
`GraphQlRSocketHandler` can be used a delegate from an `@Controller` that is mapped to
the route for GraphQL requests. For example:
include::code:GraphQlRSocketController[]
[[server.interception]]
== Interception
Server transports allow intercepting requests before and after the GraphQL Java engine is
called to process a request.
[[server.interception.web]]
=== `WebGraphQlInterceptor`
<<server.transports.http>> and <<server.transports.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`:
include::code:RequestHeaderInterceptor[]
<1> Interceptor adds HTTP request header value into GraphQLContext
<2> Data controller method accesses the value
Reversely, an interceptor can access values added to the `GraphQLContext` by a controller:
include::code:ResponseHeaderInterceptor[]
<1> Controller adds value to the `GraphQLContext`
<2> Interceptor uses the value to add an HTTP response header
`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`:
include::code:RequestErrorInterceptor[]
<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].
[[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`.

View File

@@ -1,120 +0,0 @@
= Spring for GraphQL Documentation
Brian Clozel; Andreas Marek; Rossen Stoyanchev; Mark Paluch
include::attributes.adoc[]
[[overview]]
== Overview
Spring for GraphQL provides support for Spring applications built on
https://www.graphql-java.com/[GraphQL Java]. It is a joint collaboration between the
GraphQL Java team and Spring engineering.
Spring for GraphQL is the successor of the
https://github.com/graphql-java/graphql-java-spring[GraphQL Java Spring] project from
the GraphQL Java team. It aims to be the foundation for all Spring, GraphQL applications.
Please, use our https://github.com/spring-projects/spring-graphql/issues[issue tracker]
to report a problem, discuss a design issue, or to request a feature.
Check the https://github.com/spring-projects/spring-graphql/wiki[Wiki].
for what's new, baseline requirements, and upgrade notes, and other cross-version information.
To get started, see the <<boot-starter>> and <<samples>> sections.
include::includes/transports.adoc[leveloffset=+1]
include::includes/request-execution.adoc[leveloffset=+1]
include::includes/data.adoc[leveloffset=+1]
include::includes/controllers.adoc[leveloffset=+1]
[[security]]
== Security
The path to a <<server.transports.http, Web>> GraphQL endpoint can be secured with HTTP
URL security to ensure that only authenticated users can access it. This does not,
however, differentiate among different GraphQL requests on such a shared endpoint on
a single URL.
To apply more fine-grained security, add Spring Security annotations such as
`@PreAuthorize` or `@Secured` to service methods involved in fetching specific parts of
the GraphQL response. This should work due to <<execution.context>> that aims to make
Security, and other context, available at the data fetching level.
The 1.0.x branch of this repository contains samples for
{github-10x-branch}/samples/webmvc-http-security[Spring MVC] and for
{github-10x-branch}/samples/webflux-security[WebFlux].
include::includes/observability.adoc[leveloffset=+1]
include::includes/graalvm-native.adoc[leveloffset=+1]
include::includes/client.adoc[leveloffset=+1]
include::includes/graphiql.adoc[leveloffset=+1]
include::includes/testing.adoc[leveloffset=+1]
[[boot-starter]]
== Boot Starter
Spring Boot provides a starter for building GraphQL applications with Spring for GraphQL.
For version information, see the
https://github.com/spring-projects/spring-graphql/wiki/Spring-for-GraphQL-Versions[Spring for GraphQL Versions] wiki page.
The easiest way to get started is via https://start.spring.io by selecting
"Spring for GraphQL" along with an underlying transport such as Spring MVC of WebFlux over
HTTP or WebSocket, or over RSocket. Refer to the
https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.graphql[Spring for GraphQL Starter]
section in the Spring Boot reference for details on supported transports, auto-configuration related
features, and more. For testing support, see
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.spring-boot-applications.spring-graphql-tests[Auto-Configured GraphQL Tests].
For further reference, check the following GraphQL related:
- https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.metrics.supported.spring-graphql[Metrics]
- https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.web[Properties]
- https://docs.spring.io/spring-boot/docs/current/reference/html/auto-configuration-classes.html#appendix.auto-configuration-classes.core[Auto-Configuration Classes]
[[samples]]
== Samples
Check out the https://spring.io/guides/gs/graphql-server/["Building a GraphQL service"] Getting Started Guide.
In addition, the 1.0.x branch of this repository contains {github-10x-branch}/samples[sample applications] for various scenarios.
Those samples do not exist in the `main` branch and are planned to be
https://github.com/spring-projects/spring-graphql/issues/208[moved out] into a separate repository.
To run those samples, check out the 1.0.x branch run their main application classes from your IDE,
or from the command line:
[source,bash,indent=0,subs="verbatim,quotes"]
----
$ ./gradlew :samples:{sample-directory-name}:bootRun
----