Files
spring-graphql/spring-graphql-docs/src/docs/asciidoc/client.adoc
rstoyanchev 696f2cd02f Refine documentSource locations
Closes gh-338
2022-04-01 21:39:42 +01:00

443 lines
12 KiB
Plaintext

include::attributes.adoc[]
[[client]]
= Client
Spring for GraphQL includes client support for executing GraphQL requests over HTTP,
WebSocket, and RSocket.
[[client-graphqlclient]]
== `GraphQlClient`
`GraphQlClient` defines a common workflow for executing GraphQL requests and
subscriptions that is independent of and agnostic to the underlying transport. To create
an instance, you'll need to start from either the <<client-httpgraphqlclient,
HttpGraphQlClient>> or the <<client-websocketgraphqlclient,WebSocketGraphQlClient>>
extensions.
Each `GraphQlClient` extension provides a transport specific `Builder`. There is also a
shared, base <<client-graphqlclient-builder>> in `GraphQlClient` with common options for
all extensions.
[[client-httpgraphqlclient]]
=== HTTP
`HttpGraphQlClient` uses
{spring-framework-ref-docs}/web-reactive.html#webflux-client[WebClient] to execute
GraphQL requests over HTTP.
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
----
The `HttpGraphQlClient` extension is nothing but a `GraphQlClient` with a specialized
builder. Once created, it exposes the same workflow for request execution that is
independent of the underlying transport.
This means you can only configure HTTP request details at build time, and
those apply to all requests through that client instance. To change HTTP request
details, use `mutate()` on an existing `HttpGraphQlClient` to create another
instance with different configuration:
[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` uses
{spring-framework-ref-docs}/web-reactive.html#webflux-websocket-client[WebSocketClient]
from Spring WebFlux to execute GraphQL requests over WebSocket:
[source,java,indent=0,subs="verbatim,quotes"]
----
String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();
----
Once created, `WebSocketGraphQlClient` exposes the same transport agnostic workflow for
request execution as any `GrahQlClient`. To change any transport details, use `mutate()`
on an existing `WebSocketGraphQlClient` to create another with different configuration:
[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-rsocketgraphqlclient]]
=== RSocket
`RSocketGraphQlClient` uses
{spring-framework-ref-docs}/web-reactive.html#rsocket-requester[RSocketRequester]
to execute GraphQL requests over RSocket requests.
[source,java,indent=0,subs="verbatim,quotes"]
----
RSocketGraphQlClient graphQlClient =
RSocketGraphQlClient.builder()
.websocket(URI.create("http://localhost:8080/graphql"))
.build();
----
Once created, `RSocketGraphQlClient` exposes the same transport agnostic workflow for
request execution as any `GrahQlClient`.
[[client-websocketgraphqlclient-connection]]
==== Connection
A connection is established transparently when requests are made. There is only one
shared, active connection at a time. If the connection is lost, it is re-established on
the next request.
`WebSocketGraphQlClient` also exposes lifecycle methods:
- `start()` - connect the WebSocket and initialize the GraphQL session. This can be used
on startup up to be ready for requests, but it is not required.
- `stop()` - cancels ongoing requests and subscriptions, and closes the connection. A
stopped client rejects new requests. Use `start()` to re-establish the connection and
allow requests again.
[[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-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();
// ...
ResponseField 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>
}
ResponseField 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/project.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 "project.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. Currently, only
the WebSocket transport supports GraphQL streams, so you'll need to create a
<<client-websocketgraphqlclient,WebSocketGraphQlClient>>.
[[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);
----
A subscription stream may end with:
- `SubscriptionErrorException` if the server ends the
subscription with an explicit "error" message that contains one or more GraphQL errors.
The exception provides access to the GraphQL errors decoded from that message.
- `GraphQlTransportException` such as `WebSocketDisconnectedException` if the underlying
connection is closed or lost in which case you can use the `retry` operator to reestablish
the connection and start the subscription again.
[[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...
}
ResponseField 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();
----