Skip to content

Commit afd1f49

Browse files
committed
refactor to handle GraphQL Query and Mutation requests
issue spring-projects#3501
1 parent f43dda2 commit afd1f49

File tree

4 files changed

+297
-6
lines changed

4 files changed

+297
-6
lines changed
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@
2525
import reactor.core.publisher.Mono;
2626

2727
/**
28+
* A <code>MessageHandler</code> capable of fielding GraphQL Query and Mutation requests.
2829
*
2930
* @author Daniel Frey
3031
*/
31-
public class GraphqlQueryMessageHandler extends AbstractReplyProducingMessageHandler {
32+
public class GraphqlQueryMutationMessageHandler extends AbstractReplyProducingMessageHandler {
3233

3334
private final GraphQlService graphQlService;
3435

35-
public GraphqlQueryMessageHandler(final GraphQlService graphQlService) {
36+
public GraphqlQueryMutationMessageHandler(final GraphQlService graphQlService) {
3637
this.graphQlService = graphQlService;
3738
setAsync(true);
3839
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.graphql.outbound;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.time.Duration;
22+
import java.util.Collections;
23+
import java.util.Map;
24+
import java.util.Objects;
25+
import java.util.UUID;
26+
27+
import org.junit.jupiter.api.Test;
28+
29+
import org.springframework.beans.factory.annotation.Autowired;
30+
import org.springframework.context.annotation.Bean;
31+
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.core.io.ClassPathResource;
33+
import org.springframework.graphql.GraphQlService;
34+
import org.springframework.graphql.RequestInput;
35+
import org.springframework.graphql.data.method.annotation.Argument;
36+
import org.springframework.graphql.data.method.annotation.MutationMapping;
37+
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
38+
import org.springframework.graphql.execution.BatchLoaderRegistry;
39+
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
40+
import org.springframework.graphql.execution.ExecutionGraphQlService;
41+
import org.springframework.graphql.execution.GraphQlSource;
42+
import org.springframework.integration.channel.FluxMessageChannel;
43+
import org.springframework.integration.channel.QueueChannel;
44+
import org.springframework.integration.config.EnableIntegration;
45+
import org.springframework.integration.dsl.IntegrationFlow;
46+
import org.springframework.integration.dsl.IntegrationFlows;
47+
import org.springframework.integration.dsl.MessageChannels;
48+
import org.springframework.messaging.Message;
49+
import org.springframework.messaging.MessageHandlingException;
50+
import org.springframework.messaging.PollableChannel;
51+
import org.springframework.messaging.support.ErrorMessage;
52+
import org.springframework.messaging.support.MessageBuilder;
53+
import org.springframework.stereotype.Controller;
54+
import org.springframework.stereotype.Repository;
55+
import org.springframework.test.annotation.DirtiesContext;
56+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
57+
58+
import graphql.ExecutionResult;
59+
import graphql.ExecutionResultImpl;
60+
import reactor.core.publisher.Flux;
61+
import reactor.core.publisher.Mono;
62+
import reactor.test.StepVerifier;
63+
64+
/**
65+
*
66+
* @author Daniel Frey
67+
*
68+
*/
69+
@SpringJUnitConfig(GraphqlMutationMessageHandlerTests.TestConfig.class)
70+
@DirtiesContext
71+
public class GraphqlMutationMessageHandlerTests {
72+
73+
@Autowired
74+
private FluxMessageChannel inputChannel;
75+
76+
@Autowired
77+
private FluxMessageChannel resultChannel;
78+
79+
@Autowired
80+
private PollableChannel errorChannel;
81+
82+
@Autowired
83+
UpdateRepository updateRepository;
84+
85+
@Test
86+
void testHandleMessageForMutation() {
87+
88+
String fakeId = UUID.randomUUID().toString();
89+
Update expected = new Update(fakeId);
90+
91+
StepVerifier verifier = StepVerifier.create(
92+
Flux.from(this.resultChannel)
93+
.map(Message::getPayload)
94+
.cast(ExecutionResult.class)
95+
)
96+
.consumeNextWith(result -> {
97+
assertThat(result).isInstanceOf(ExecutionResultImpl.class);
98+
Map<String, Object> data = result.getData();
99+
Map<String, Object> update = (Map<String, Object>) data.get("update");
100+
assertThat(update.get("id")).isEqualTo(fakeId);
101+
102+
assertThat(this.updateRepository.current().block()).isEqualTo(expected);
103+
}
104+
)
105+
.thenCancel()
106+
.verifyLater();
107+
108+
this.inputChannel.send(
109+
MessageBuilder
110+
.withPayload(new RequestInput("mutation { update(id: \"" + fakeId + "\") { id } }", null, Collections.emptyMap()))
111+
.build()
112+
);
113+
114+
verifier.verify(Duration.ofSeconds(10));
115+
116+
StepVerifier.create(this.updateRepository.current())
117+
.expectNext(expected)
118+
.expectComplete()
119+
.verify();
120+
121+
}
122+
123+
@Test
124+
void testHandleMessageForQueryWithInvalidPayload() {
125+
126+
String fakeId = UUID.randomUUID().toString();
127+
128+
this.inputChannel.send(
129+
MessageBuilder
130+
.withPayload("mutation { update(id: \"" + fakeId + "\") { id } }")
131+
.build()
132+
);
133+
134+
Message<?> errorMessage = errorChannel.receive(10_000);
135+
assertThat(errorMessage).isNotNull()
136+
.isInstanceOf(ErrorMessage.class)
137+
.extracting(Message::getPayload)
138+
.isInstanceOf(MessageHandlingException.class)
139+
.satisfies((ex) -> assertThat((Exception) ex)
140+
.hasMessageContaining(
141+
"Message payload needs to be 'org.springframework.graphql.RequestInput'"));
142+
143+
}
144+
145+
@Controller
146+
static class GraphqlMutationController {
147+
148+
final UpdateRepository updateRepository;
149+
150+
GraphqlMutationController(UpdateRepository updateRepository) {
151+
this.updateRepository = updateRepository;
152+
}
153+
154+
@MutationMapping
155+
public Mono<Update> update(@Argument String id) {
156+
return this.updateRepository.save(new Update(id));
157+
}
158+
159+
}
160+
161+
@Repository
162+
static class UpdateRepository {
163+
164+
private Update current;
165+
166+
Mono<Update> save(Update update) {
167+
this.current = update;
168+
return Mono.justOrEmpty(this.current);
169+
}
170+
171+
Mono<Update> current() {
172+
return Mono.just(this.current);
173+
}
174+
175+
}
176+
177+
@Configuration
178+
@EnableIntegration
179+
static class TestConfig {
180+
181+
@Bean
182+
GraphqlQueryMutationMessageHandler handler(GraphQlService graphQlService) {
183+
184+
return new GraphqlQueryMutationMessageHandler(graphQlService);
185+
}
186+
187+
@Bean
188+
IntegrationFlow graphqlQueryMessageHandlerFlow(GraphqlQueryMutationMessageHandler handler) {
189+
190+
return IntegrationFlows.from(MessageChannels.flux("inputChannel"))
191+
.handle(handler)
192+
.channel(c -> c.flux("resultChannel"))
193+
.get();
194+
}
195+
196+
@Bean
197+
PollableChannel errorChannel() {
198+
199+
return new QueueChannel();
200+
}
201+
202+
@Bean
203+
UpdateRepository updateRepository() {
204+
return new UpdateRepository();
205+
}
206+
207+
@Bean
208+
GraphqlMutationController graphqlMutationController(final UpdateRepository updateRepository) {
209+
210+
return new GraphqlMutationController(updateRepository);
211+
}
212+
213+
@Bean
214+
GraphQlService graphQlService(GraphQlSource graphQlSource, BatchLoaderRegistry batchLoaderRegistry) {
215+
216+
ExecutionGraphQlService service = new ExecutionGraphQlService(graphQlSource);
217+
service.addDataLoaderRegistrar(batchLoaderRegistry);
218+
219+
return service;
220+
}
221+
222+
@Bean
223+
GraphQlSource graphQlSource(AnnotatedControllerConfigurer annotatedDataFetcherConfigurer) {
224+
225+
return GraphQlSource.builder()
226+
.schemaResources(new ClassPathResource("graphql/test-schema.graphqls"))
227+
.configureRuntimeWiring(annotatedDataFetcherConfigurer)
228+
.build();
229+
}
230+
231+
@Bean
232+
AnnotatedControllerConfigurer annotatedDataFetcherConfigurer() {
233+
234+
return new AnnotatedControllerConfigurer();
235+
}
236+
237+
@Bean
238+
BatchLoaderRegistry batchLoaderRegistry() {
239+
240+
return new DefaultBatchLoaderRegistry();
241+
}
242+
243+
}
244+
245+
static class Update {
246+
247+
private final String id;
248+
249+
Update(final String id) {
250+
this.id = id;
251+
}
252+
253+
String getId() {
254+
return this.id;
255+
}
256+
257+
@Override
258+
public boolean equals(Object o) {
259+
if (this == o) {
260+
return true;
261+
}
262+
if (!(o instanceof Update)) {
263+
return false;
264+
}
265+
Update update = (Update) o;
266+
return getId().equals(update.getId());
267+
}
268+
269+
@Override
270+
public int hashCode() {
271+
return Objects.hash(getId());
272+
}
273+
}
274+
275+
}

spring-integration-graphql/src/test/java/org/springframework/integration/graphql/outbound/GraphqlQueryMessageHandlerTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,13 @@ public Mono<QueryResult> testQuery() {
137137
static class TestConfig {
138138

139139
@Bean
140-
GraphqlQueryMessageHandler handler(GraphQlService graphQlService) {
140+
GraphqlQueryMutationMessageHandler handler(GraphQlService graphQlService) {
141141

142-
return new GraphqlQueryMessageHandler(graphQlService);
142+
return new GraphqlQueryMutationMessageHandler(graphQlService);
143143
}
144144

145145
@Bean
146-
IntegrationFlow graphqlQueryMessageHandlerFlow(GraphqlQueryMessageHandler handler) {
146+
IntegrationFlow graphqlQueryMessageHandlerFlow(GraphqlQueryMutationMessageHandler handler) {
147147

148148
return IntegrationFlows.from(MessageChannels.flux("inputChannel"))
149149
.handle(handler)
@@ -176,7 +176,7 @@ GraphQlService graphQlService(GraphQlSource graphQlSource, BatchLoaderRegistry b
176176
GraphQlSource graphQlSource(AnnotatedControllerConfigurer annotatedDataFetcherConfigurer) {
177177

178178
return GraphQlSource.builder()
179-
.schemaResources(new ClassPathResource("graphql/test-query-schema.graphqls"))
179+
.schemaResources(new ClassPathResource("graphql/test-schema.graphqls"))
180180
.configureRuntimeWiring(annotatedDataFetcherConfigurer)
181181
.build();
182182
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type Query {
2+
testQuery: QueryResult
3+
}
4+
5+
type Mutation {
6+
update(id: String!): Update!
7+
}
8+
9+
type QueryResult {
10+
id: String
11+
}
12+
13+
type Update {
14+
id: String
15+
}

0 commit comments

Comments
 (0)