Skip to content

Add documentation for GraphQL support #3756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.RequestInput;
import org.springframework.graphql.ExecutionGraphQlRequest;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.support.DefaultExecutionGraphQlRequest;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.expression.SupplierExpression;
Expand All @@ -44,7 +45,7 @@
*/
public class GraphQlMessageHandler extends AbstractReplyProducingMessageHandler {

private final GraphQlService graphQlService;
private final ExecutionGraphQlService graphQlService;

private StandardEvaluationContext evaluationContext;

Expand All @@ -60,7 +61,7 @@ public class GraphQlMessageHandler extends AbstractReplyProducingMessageHandler
private Expression executionIdExpression =
new FunctionExpression<Message<?>>(message -> message.getHeaders().getId());

public GraphQlMessageHandler(final GraphQlService graphQlService) {
public GraphQlMessageHandler(final ExecutionGraphQlService graphQlService) {
Assert.notNull(graphQlService, "'graphQlService' must not be null");
this.graphQlService = graphQlService;
setAsync(true);
Expand Down Expand Up @@ -135,21 +136,21 @@ protected final void doInit() {

@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
RequestInput requestInput;
ExecutionGraphQlRequest graphQlRequest;

if (requestMessage.getPayload() instanceof RequestInput) {
requestInput = (RequestInput) requestMessage.getPayload();
if (requestMessage.getPayload() instanceof ExecutionGraphQlRequest) {
graphQlRequest = (ExecutionGraphQlRequest) requestMessage.getPayload();
}
else {
Assert.notNull(this.operationExpression, "'operationExpression' must not be null");
String query = evaluateOperationExpression(requestMessage);
String operationName = evaluateOperationNameExpression(requestMessage);
Map<String, Object> variables = evaluateVariablesExpression(requestMessage);
String id = evaluateExecutionIdExpression(requestMessage);
requestInput = new RequestInput(query, operationName, variables, id, this.locale);
graphQlRequest = new DefaultExecutionGraphQlRequest(query, operationName, variables, id, this.locale);
}

return this.graphQlService.execute(requestInput);
return this.graphQlService.execute(graphQlRequest);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.RequestInput;
import org.springframework.graphql.RequestOutput;
import org.springframework.graphql.ExecutionGraphQlRequest;
import org.springframework.graphql.ExecutionGraphQlResponse;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.DefaultExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.support.DefaultExecutionGraphQlRequest;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
Expand Down Expand Up @@ -94,18 +95,19 @@ void testHandleMessageForQueryWithRequestInputProvided() {
StepVerifier.create(
Flux.from(this.resultChannel)
.map(Message::getPayload)
.cast(RequestOutput.class)
.cast(ExecutionGraphQlResponse.class)
)
.consumeNextWith(result -> {
assertThat(result).isInstanceOf(RequestOutput.class);
assertThat(result).isInstanceOf(ExecutionGraphQlResponse.class);
Map<String, Object> data = result.getData();
Map<String, Object> testQuery = (Map<String, Object>) data.get("testQuery");
assertThat(testQuery.get("id")).isEqualTo("test-data");
})
.thenCancel()
.verifyLater();

RequestInput payload = new RequestInput("{ testQuery { id } }", null, null, UUID.randomUUID().toString(), null);
ExecutionGraphQlRequest payload = new DefaultExecutionGraphQlRequest("{ testQuery { id } }", null, null,
UUID.randomUUID().toString(), null);
this.inputChannel.send(MessageBuilder.withPayload(payload).build());

verifier.verify(Duration.ofSeconds(10));
Expand All @@ -120,11 +122,11 @@ void testHandleMessageForQueryWithQueryProvided() {
Locale locale = Locale.getDefault();
this.graphQlMessageHandler.setLocale(locale);

Mono<RequestOutput> resultMono =
(Mono<RequestOutput>) this.graphQlMessageHandler.handleRequestMessage(new GenericMessage<>(fakeQuery));
Mono<ExecutionGraphQlResponse> resultMono =
(Mono<ExecutionGraphQlResponse>) this.graphQlMessageHandler.handleRequestMessage(new GenericMessage<>(fakeQuery));
StepVerifier.create(resultMono)
.consumeNextWith(result -> {
assertThat(result).isInstanceOf(RequestOutput.class);
assertThat(result).isInstanceOf(ExecutionGraphQlResponse.class);
Map<String, Object> data = result.getData();
Map<String, Object> testQuery = (Map<String, Object>) data.get("testQuery");
assertThat(testQuery.get("id")).isEqualTo("test-data");
Expand All @@ -142,10 +144,10 @@ void testHandleMessageForMutationWithRequestInputProvided() {
StepVerifier verifier = StepVerifier.create(
Flux.from(this.resultChannel)
.map(Message::getPayload)
.cast(RequestOutput.class)
.cast(ExecutionGraphQlResponse.class)
)
.consumeNextWith(result -> {
assertThat(result).isInstanceOf(RequestOutput.class);
assertThat(result).isInstanceOf(ExecutionGraphQlResponse.class);
Map<String, Object> data = result.getData();
Map<String, Object> update = (Map<String, Object>) data.get("update");
assertThat(update.get("id")).isEqualTo(fakeId);
Expand All @@ -156,8 +158,8 @@ void testHandleMessageForMutationWithRequestInputProvided() {
.thenCancel()
.verifyLater();

RequestInput payload =
new RequestInput("mutation { update(id: \"" + fakeId + "\") { id } }", null, null,
ExecutionGraphQlRequest payload =
new DefaultExecutionGraphQlRequest("mutation { update(id: \"" + fakeId + "\") { id } }", null, null,
UUID.randomUUID().toString(), null);
this.inputChannel.send(MessageBuilder.withPayload(payload).build());

Expand All @@ -175,8 +177,8 @@ void testHandleMessageForSubscriptionWithRequestInputProvided() {
StepVerifier verifier = StepVerifier.create(
Flux.from(this.resultChannel)
.map(Message::getPayload)
.cast(RequestOutput.class)
.mapNotNull(RequestOutput::getData)
.cast(ExecutionGraphQlResponse.class)
.mapNotNull(ExecutionGraphQlResponse::getData)
.cast(SubscriptionPublisher.class)
.map(Flux::from)
.flatMap(data -> data)
Expand All @@ -195,8 +197,9 @@ void testHandleMessageForSubscriptionWithRequestInputProvided() {
.thenCancel()
.verifyLater();

RequestInput payload =
new RequestInput("subscription { results { id } }", null, null, UUID.randomUUID().toString(), null);
ExecutionGraphQlRequest payload =
new DefaultExecutionGraphQlRequest("subscription { results { id } }", null, null,
UUID.randomUUID().toString(), null);
this.inputChannel.send(MessageBuilder.withPayload(payload).build());

verifier.verify(Duration.ofSeconds(10));
Expand Down Expand Up @@ -279,8 +282,7 @@ Mono<Update> current() {
static class TestConfig {

@Bean
GraphQlMessageHandler handler(GraphQlService graphQlService) {

GraphQlMessageHandler handler(ExecutionGraphQlService graphQlService) {
return new GraphQlMessageHandler(graphQlService);
}

Expand Down Expand Up @@ -308,8 +310,8 @@ GraphQlController graphqlQueryController(UpdateRepository updateRepository) {
}

@Bean
GraphQlService graphQlService(GraphQlSource graphQlSource) {
return new ExecutionGraphQlService(graphQlSource);
ExecutionGraphQlService graphQlService(GraphQlSource graphQlSource) {
return new DefaultExecutionGraphQlService(graphQlSource);
}

@Bean
Expand Down
6 changes: 6 additions & 0 deletions src/reference/asciidoc/endpoint-summary.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ The following table summarizes the various endpoints with quick links to the app
| N
| N

| *GraphQL*
| N
| N
| N
| <<./graphql.adoc#graphql-outbound-gateway,GraphQL Outbound Gateway>>

| *HTTP*
| <<./http.adoc#http-namespace,HTTP Namespace Support>>
| <<./http.adoc#http-namespace,HTTP Namespace Support>>
Expand Down
84 changes: 83 additions & 1 deletion src/reference/asciidoc/graphql.adoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[[graphql]]
== GraphQL Support

Spring Integration provides support for GraphQL.
Spring Integration provides channel adapters for interaction with https://graphql.org/[GraphQL] protocol.
The implementation is based on the https://spring.io/projects/spring-graphql[Spring for GraphQL].

You need to include this dependency into your project:

Expand All @@ -21,3 +22,84 @@ You need to include this dependency into your project:
compile "org.springframework.integration:spring-integration-graphql:{project-version}"
----
====

[[graphql-outbound-gateway]]
=== GraphQL Outbound Gateway

The `GraphQlMessageHandler` is an `AbstractReplyProducingMessageHandler` extension representing an outbound gateway contract to perform GraphQL `query`, `mutation` or `subscription` operation and produce their result.
It requires a `org.springframework.graphql.ExecutionGraphQlService` for execution of `operation`, which can be configured statically or via SpEL expression against a request message.
The `operationName` is optional and also can be configured statically or via SpEL expression.
The `variablesExpression` is also optional and used for parametrized operations.
The `locale` is optional and used for operation execution context in the https://www.graphql-java.com/[GraphQL Java] library.
The `executionId` can be configured via SpEL expression and defaults to `id` header of the request message.

If the payload of request message is an instance of `ExecutionGraphQlRequest`, then there's no any setup actions are performed in the `GraphQlMessageHandler` and such an input is used as is for the `ExecutionGraphQlService.execute()`.
Otherwise, the `operation`, `operationName`, `variables` and `executionId` are determined against request message using SpEL expressions mentioned above.

The `GraphQlMessageHandler` is a reactive streams component and produces a `Mono<ExecutionGraphQlResponse>` reply as a result of the `ExecutionGraphQlService.execute(ExecutionGraphQlRequest)`.
Such a `Mono` is subscribed by the framework in the `ReactiveStreamsSubscribableChannel` output channel or in the `AbstractMessageProducingHandler` asynchronously when the output channel is not reactive.
See documentation for the `ExecutionGraphQlResponse` how to process the GraphQL operation result.

====
[source, java]
----
@Bean
GraphQlMessageHandler handler(ExecutionGraphQlService graphQlService) {
GraphQlMessageHandler graphQlMessageHandler = new GraphQlMessageHandler(graphQlService);
graphQlMessageHandler.setOperation("""
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}""");
graphQlMessageHandler.setVariablesExpression(new SpelExpressionParser().parseExpression("{episode:'JEDI'}"));
return graphQlMessageHandler;
}

@Bean
IntegrationFlow graphqlQueryMessageHandlerFlow(GraphQlMessageHandler handler) {
return IntegrationFlows.from(MessageChannels.flux("inputChannel"))
.handle(handler)
.channel(c -> c.flux("resultChannel"))
.get();
}

@Bean
ExecutionGraphQlService graphQlService(GraphQlSource graphQlSource) {
return new DefaultExecutionGraphQlService(graphQlSource);
}

@Bean
GraphQlSource graphQlSource(AnnotatedControllerConfigurer annotatedDataFetcherConfigurer) {
return GraphQlSource.builder()
.schemaResources(new ClassPathResource("graphql/test-schema.graphqls"))
.configureRuntimeWiring(annotatedDataFetcherConfigurer)
.build();
}

@Bean
AnnotatedControllerConfigurer annotatedDataFetcherConfigurer() {
return new AnnotatedControllerConfigurer();
}
----
====

The special treatment should be applied for the result of a subscription operation.
In this case the `RequestOutput.getData()` returns a `SubscriptionPublisher` which has to subscribed and processed manually.
Or it can be flat-mapped via plain service activator to the reply for the `FluxMessageChannel`:

====
[source, java]
----
@ServiceActivator(inputChannel = "graphQlResultChannel", outputChannel="graphQlSubscriptionChannel")
public SubscriptionPublisher obtainSubscriptionResult(RequestOutput output) {
return output.getData(0);
}
----
====

Such an outbound gateway can be used not only for GraphQL request via HTTP, but from any upstream endpoint which produces or carries a GraphQL operation or its arguments in the message.
The result of the `GraphQlMessageHandler` handling can be produces as a reply to the upstream request or sent downstream for further processing in the integration flow.
2 changes: 1 addition & 1 deletion src/reference/asciidoc/reactive-streams.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ public class MainFlow {
----
====

Currently, Spring Integration provides channel adapter (or gateway) implementations for <<./webflux.adoc#webflux,WebFlux>>, <<./rsocket.adoc#rsocket,RSocket>>, <<./mongodb.adoc#mongodb,MongoDb>>, <<./r2dbc.adoc#r2dbc,R2DBC>>, <<./zeromq.adoc#zeromq,ZeroMQ>>.
Currently, Spring Integration provides channel adapter (or gateway) implementations for <<./webflux.adoc#webflux,WebFlux>>, <<./rsocket.adoc#rsocket,RSocket>>, <<./mongodb.adoc#mongodb,MongoDb>>, <<./r2dbc.adoc#r2dbc,R2DBC>>, <<./zeromq.adoc#zeromq,ZeroMQ>>, <<./graphql.adoc#graphql,GraphQL>>.
The <<./redis.adoc#redis-stream-outbound,Redis Stream Channel Adapters>> are also reactive and uses `ReactiveStreamOperations` from Spring Data.
Also, an https://github.com/spring-projects/spring-integration-extensions/tree/main/spring-integration-cassandra[Apache Cassandra Extension] provides a `MessageHandler` implementation for the Cassandra reactive driver.
More reactive channel adapters are coming, for example for Apache Kafka in <<./kafka.adoc#kafka,Kafka>> based on the `ReactiveKafkaProducerTemplate` and `ReactiveKafkaConsumerTemplate` from https://spring.io/projects/spring-kafka[Spring for Apache Kafka] etc.
Expand Down