Prior to this commit, the "client" and "testing" sections of the reference documentation were using inline code snippets. Because of this, several snippets were out of date or invalid. This commit moves all those code snippets to actual Java classes compiled with the documentation. Fixes gh-1042
437 lines
15 KiB
Plaintext
437 lines
15 KiB
Plaintext
[[client]]
|
|
= Client
|
|
|
|
Spring for GraphQL includes client support to execute GraphQL requests over HTTP,
|
|
WebSocket, and RSocket.
|
|
|
|
|
|
|
|
[[client.graphqlclient]]
|
|
== `GraphQlClient`
|
|
|
|
`GraphQlClient` defines a common workflow for GraphQL requests independent of the underlying
|
|
transport, so the way you perform requests is the same no matter what transport is in use.
|
|
|
|
The following transport specific `GraphQlClient` extensions are available:
|
|
|
|
- xref:client.adoc#client.httpsyncgraphqlclient[HttpSyncGraphQlClient]
|
|
- xref:client.adoc#client.httpgraphqlclient[HttpGraphQlClient]
|
|
- xref:client.adoc#client.websocketgraphqlclient[WebSocketGraphQlClient]
|
|
- xref:client.adoc#client.rsocketgraphqlclient[RSocketGraphQlClient]
|
|
|
|
Each defines a `Builder` with options relevant to the transport. All builders extend
|
|
from a common, base GraphQlClient xref:client.adoc#client.graphqlclient.builder[`Builder`]
|
|
with options applicable to all transports.
|
|
|
|
Once `GraphQlClient` is built you can begin to make xref:client.adoc#client.requests[requests].
|
|
|
|
Typically, the GraphQL operation for a request is provided as text. Alternatively, you
|
|
can use https://github.com/Netflix/dgs-codegen[DGS Codegen] client API classes through
|
|
xref:client.adoc#client.dgsgraphqlclient[DgsGraphQlClient], which can wrap any of the
|
|
above `GraphQlClient` extensions.
|
|
|
|
|
|
|
|
[[client.httpsyncgraphqlclient]]
|
|
=== HTTP Sync
|
|
|
|
`HttpSyncGraphQlClient` uses
|
|
{spring-framework-ref-docs}/integration/rest-clients.html#rest-restclient[RestClient]
|
|
to execute GraphQL requests over HTTP through a blocking transport contract and chain of
|
|
interceptors.
|
|
|
|
include-code::SyncClientUsage[tag=create,indent=0]
|
|
|
|
Once `HttpSyncGraphQlClient` is created, you can begin to
|
|
xref:client.adoc#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 `HttpSyncGraphQlClient` to create a new instance with customized settings:
|
|
|
|
include-code::SyncClientUsage[tag=mutate,indent=0]
|
|
|
|
|
|
[[client.httpgraphqlclient]]
|
|
=== HTTP
|
|
|
|
`HttpGraphQlClient` uses
|
|
{spring-framework-ref-docs}/web/webflux-webclient.html[WebClient] to execute
|
|
GraphQL requests over HTTP through a non-blocking transport contract and chain of
|
|
interceptors.
|
|
|
|
include-code::ClientUsage[tag=create,indent=0]
|
|
|
|
Once `HttpGraphQlClient` is created, you can begin to
|
|
xref:client.adoc#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:
|
|
|
|
include-code::ClientUsage[tag=mutate,indent=0]
|
|
|
|
|
|
[[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:
|
|
|
|
include-code::WebSocketClientUsage[tag=create,indent=0]
|
|
|
|
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
|
|
xref:client.adoc#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:
|
|
|
|
include-code::WebSocketClientUsage[tag=mutate,indent=0]
|
|
|
|
`WebSocketGraphQlClient` supports sending periodic ping messages to keep the connection
|
|
active when no other messages are sent or received. You can enable that as follows:
|
|
|
|
include-code::WebSocketClientUsage[tag=keepAlive,indent=0]
|
|
|
|
|
|
[[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
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
xref:client.adoc#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.
|
|
|
|
include-code::RSocketClientUsage[tag=create,indent=0]
|
|
|
|
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
|
|
xref:client.adoc#client.requests[execute requests] using the same API, independent of the underlying
|
|
transport.
|
|
|
|
|
|
|
|
[[client.graphqlclient.builder]]
|
|
=== Builder
|
|
|
|
`GraphQlClient` defines a parent `BaseBuilder` 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
|
|
- xref:client.adoc#client.interception[Interception] of executed requests
|
|
|
|
`BaseBuilder` is further extended by the following:
|
|
|
|
- `SyncBuilder` - blocking execution stack with a chain of ``SyncGraphQlInterceptor``'s.
|
|
- `Builder` - non-blocking execution stack with chain of ``GraphQlInterceptor``'s.
|
|
|
|
|
|
|
|
[[client.requests]]
|
|
== Requests
|
|
|
|
Once you have a xref:client.adoc#client.graphqlclient[`GraphQlClient`], you can begin to perform requests via
|
|
xref:client.adoc#client.requests.retrieve[retrieve] or xref:client.adoc#client.requests.execute[execute]
|
|
methods.
|
|
|
|
|
|
|
|
[[client.requests.retrieve]]
|
|
=== Retrieve
|
|
|
|
The below retrieves and decodes the data for a query:
|
|
|
|
[tabs]
|
|
======
|
|
Sync::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
----
|
|
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=retrieveSync,indent=0]
|
|
----
|
|
|
|
Non-Blocking::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
----
|
|
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=retrieve,indent=0]
|
|
----
|
|
======
|
|
|
|
<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
|
|
xref:client.adoc#client.requests.document-source[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:
|
|
|
|
[tabs]
|
|
======
|
|
Sync::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
----
|
|
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=fieldErrorSync,indent=0]
|
|
----
|
|
|
|
Non-Blocking::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
----
|
|
include::{include-java}/client/requests/retrieve/Retrieve.java[tag=fieldError,indent=0]
|
|
----
|
|
======
|
|
|
|
|
|
|
|
[[client.requests.execute]]
|
|
=== Execute
|
|
|
|
xref:client.adoc#client.requests.retrieve[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:
|
|
|
|
[tabs]
|
|
======
|
|
Sync::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
----
|
|
include::{include-java}/client/requests/execute/Execute.java[tag=executeSync,indent=0]
|
|
----
|
|
|
|
Non-Blocking::
|
|
+
|
|
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
----
|
|
include::{include-java}/client/requests/execute/Execute.java[tag=execute,indent=0]
|
|
----
|
|
======
|
|
|
|
<1> The response does not have data, only errors
|
|
<2> Field that was set to `null` by its `DataFetcher`
|
|
<3> Field that is `null` and has an associated error
|
|
<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"]
|
|
----
|
|
include::{include-java}/client/requests/documentsource/DocumentSource.java[tag=documentSource,indent=0]
|
|
----
|
|
<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` xref:client.adoc#client.graphqlclient.builder[Builder] to customize the
|
|
`DocumentSource` for loading documents by names.
|
|
|
|
|
|
|
|
|
|
[[client.subscriptions]]
|
|
== Subscription Requests
|
|
|
|
Subscription requests require a client transport that is capable of streaming data.
|
|
You will need to create a `GraphQlClient` that support this:
|
|
|
|
- xref:client.adoc#client.httpgraphqlclient[HttpGraphQlClient] with Server-Sent Events
|
|
- xref:client.adoc#client.websocketgraphqlclient[WebSocketGraphQlClient] with WebSocket
|
|
- xref:client.adoc#client.rsocketgraphqlclient[RSocketGraphQlClient] with RSocket
|
|
|
|
|
|
|
|
[[client.subscriptions.retrieve]]
|
|
=== Retrieve
|
|
|
|
To start a subscription stream, use `retrieveSubscription` which is similar to
|
|
xref:client.adoc#client.requests.retrieve[retrieve] for a single response but returning a stream of
|
|
responses, each decoded to some data:
|
|
|
|
include-code::RetrieveSubscription[tag=subscriptionRetrieve,indent=0]
|
|
|
|
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 terminate 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
|
|
|
|
xref:client.adoc#client.subscriptions.retrieve[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:
|
|
|
|
include-code::ExecuteSubscription[tag=subscriptionExecute,indent=0]
|
|
|
|
|
|
|
|
[[client.interception]]
|
|
== Interception
|
|
|
|
For blocking transports created with the `GraphQlClient.SyncBuilder`, you create a
|
|
`SyncGraphQlClientInterceptor` to intercept all requests through the client:
|
|
|
|
include-code::SyncInterceptor[]
|
|
|
|
For non-blocking transports created with `GraphQlClient.Builder`, you create a
|
|
`GraphQlClientInterceptor` to intercept all requests through the client:
|
|
|
|
include-code::MyInterceptor[]
|
|
|
|
Once the interceptor is created, register it through the client builder. For example:
|
|
|
|
include-code::UseInterceptor[tag=register,indent=0]
|
|
|
|
|
|
|
|
[[client.dgsgraphqlclient]]
|
|
== DGS Codegen
|
|
|
|
As an alternative to providing the operation such as a mutation, query, or subscription as
|
|
text, you can use the https://github.com/Netflix/dgs-codegen[DGS Codegen] library to
|
|
generate client API classes that let you use a fluent API to define the request.
|
|
|
|
Spring for GraphQL provides xref:client.adoc#client.dgsgraphqlclient[DgsGraphQlClient]
|
|
that wraps any `GraphQlClient` and helps to prepare the request with generated client
|
|
API classes.
|
|
|
|
For example, given the following schema:
|
|
|
|
[source,graphql,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
type Query {
|
|
books: [Book]
|
|
}
|
|
|
|
type Book {
|
|
id: ID
|
|
name: String
|
|
}
|
|
----
|
|
|
|
You can perform a request as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
HttpGraphQlClient client = ... ;
|
|
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); // <1>
|
|
|
|
List<Book> books = dgsClient.request(new BooksGraphQLQuery()) // <2>
|
|
.projection(new BooksProjectionRoot<>().id().name()) // <3>
|
|
.retrieveSync("books")
|
|
.toEntityList(Book.class);
|
|
----
|
|
|
|
<1> - Create `DgsGraphQlClient` by wrapping any `GraphQlClient`.
|
|
<2> - Specify the operation for the request.
|
|
<3> - Define the selection set.
|