Skip to content

Latest commit

 

History

History
442 lines (309 loc) · 12.4 KB

File metadata and controls

442 lines (309 loc) · 12.4 KB

Client

Spring for GraphQL includes client support for executing GraphQL requests over HTTP, WebSocket, and RSocket.

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 HttpGraphQlClient or the WebSocketGraphQlClient extensions.

Each GraphQlClient extension provides a transport specific Builder. There is also a shared, base Builder in GraphQlClient with common options for all extensions.

HTTP

HttpGraphQlClient uses {spring-framework-ref-docs}/web-reactive.html#webflux-client[WebClient] to execute GraphQL requests over HTTP.

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:

   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...

WebSocket

WebSocketGraphQlClient uses {spring-framework-ref-docs}/web-reactive.html#webflux-websocket-client[WebSocketClient] from Spring WebFlux to execute GraphQL requests over WebSocket:

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:

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...

RSocket

RSocketGraphQlClient uses {spring-framework-ref-docs}/web-reactive.html#rsocket-requester[RSocketRequester] to execute GraphQL requests over RSocket requests.

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.

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.

Interceptor

The 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:

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
	}

}

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.

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

  • Interception of executed requests

Requests

Once you have a GraphQlClient, you can begin to perform requests via retrieve() or execute() where the former is only a shortcut for the latter.

Retrieve

The below retrieves and decodes the data for a query:

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 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:

Mono<Project> projectMono = graphQlClient.document(document)
		.retrieve("project")
		.toEntity(Project.class)
		.onErrorResume(FieldAccessException.class, ex -> {
			ClientGraphQlResponse response = ex.getResponse();
			// ...
			ResponseField field = ex.getField();
			// ...
		});

Execute

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:

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

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:

src/main/resources/graphql/project.graphql
query projectReleases($slug: ID!) {
	project(slug: $slug) {
		name
		releases {
			version
		}
	}
}

You can then:

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 Builder to customize the DocumentSource for loading documents by names.

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 WebSocketGraphQlClient.

Retrieve

To start a subscription stream, use retrieveSubscription which is similar to retrieve for a single response but returning a stream of responses, each decoded to some data:

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.

Execute

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:

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

Interception

You create a GraphQlClientInterceptor to intercept all requests through a client:

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:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
		.interceptor(new MyInterceptor())
		.build();