diff --git a/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java index 48560892c9b..56cc87b80d4 100644 --- a/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java +++ b/spring-integration-graphql/src/main/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandler.java @@ -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; @@ -44,7 +45,7 @@ */ public class GraphQlMessageHandler extends AbstractReplyProducingMessageHandler { - private final GraphQlService graphQlService; + private final ExecutionGraphQlService graphQlService; private StandardEvaluationContext evaluationContext; @@ -60,7 +61,7 @@ public class GraphQlMessageHandler extends AbstractReplyProducingMessageHandler private Expression executionIdExpression = new FunctionExpression>(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); @@ -135,10 +136,10 @@ 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"); @@ -146,10 +147,10 @@ protected Object handleRequestMessage(Message requestMessage) { String operationName = evaluateOperationNameExpression(requestMessage); Map 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); } diff --git a/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java b/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java index c61f3e8179b..546b41855f6 100644 --- a/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java +++ b/spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphQlMessageHandlerTests.java @@ -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; @@ -94,10 +95,10 @@ 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 data = result.getData(); Map testQuery = (Map) data.get("testQuery"); assertThat(testQuery.get("id")).isEqualTo("test-data"); @@ -105,7 +106,8 @@ void testHandleMessageForQueryWithRequestInputProvided() { .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)); @@ -120,11 +122,11 @@ void testHandleMessageForQueryWithQueryProvided() { Locale locale = Locale.getDefault(); this.graphQlMessageHandler.setLocale(locale); - Mono resultMono = - (Mono) this.graphQlMessageHandler.handleRequestMessage(new GenericMessage<>(fakeQuery)); + Mono resultMono = + (Mono) this.graphQlMessageHandler.handleRequestMessage(new GenericMessage<>(fakeQuery)); StepVerifier.create(resultMono) .consumeNextWith(result -> { - assertThat(result).isInstanceOf(RequestOutput.class); + assertThat(result).isInstanceOf(ExecutionGraphQlResponse.class); Map data = result.getData(); Map testQuery = (Map) data.get("testQuery"); assertThat(testQuery.get("id")).isEqualTo("test-data"); @@ -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 data = result.getData(); Map update = (Map) data.get("update"); assertThat(update.get("id")).isEqualTo(fakeId); @@ -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()); @@ -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) @@ -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)); @@ -279,8 +282,7 @@ Mono current() { static class TestConfig { @Bean - GraphQlMessageHandler handler(GraphQlService graphQlService) { - + GraphQlMessageHandler handler(ExecutionGraphQlService graphQlService) { return new GraphQlMessageHandler(graphQlService); } @@ -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 diff --git a/src/reference/asciidoc/endpoint-summary.adoc b/src/reference/asciidoc/endpoint-summary.adoc index a23ecc13a73..f0cfa5a4139 100644 --- a/src/reference/asciidoc/endpoint-summary.adoc +++ b/src/reference/asciidoc/endpoint-summary.adoc @@ -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>> diff --git a/src/reference/asciidoc/graphql.adoc b/src/reference/asciidoc/graphql.adoc index a9befd66ed5..ff59bbaeb88 100644 --- a/src/reference/asciidoc/graphql.adoc +++ b/src/reference/asciidoc/graphql.adoc @@ -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: @@ -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` 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. \ No newline at end of file diff --git a/src/reference/asciidoc/reactive-streams.adoc b/src/reference/asciidoc/reactive-streams.adoc index 2825436bdbd..89c1757a251 100644 --- a/src/reference/asciidoc/reactive-streams.adoc +++ b/src/reference/asciidoc/reactive-streams.adoc @@ -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.