Skip to content
This repository was archived by the owner on Oct 25, 2021. It is now read-only.

Commit 91026ae

Browse files
authored
Merge pull request #5 from marceloverdijk/serving-over-http
#4 Add support for application/graphql content type and post query params
2 parents d7659a9 + 7e5a492 commit 91026ae

File tree

6 files changed

+362
-33
lines changed

6 files changed

+362
-33
lines changed

graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/GraphQLController.java

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package graphql.spring.web.reactive.components;
22

3-
43
import com.fasterxml.jackson.databind.ObjectMapper;
54
import graphql.ExecutionResult;
65
import graphql.Internal;
76
import graphql.spring.web.reactive.ExecutionResultHandler;
87
import graphql.spring.web.reactive.GraphQLInvocation;
98
import graphql.spring.web.reactive.GraphQLInvocationData;
109
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.HttpStatus;
1112
import org.springframework.http.MediaType;
1213
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestHeader;
1315
import org.springframework.web.bind.annotation.RequestMapping;
1416
import org.springframework.web.bind.annotation.RequestMethod;
1517
import org.springframework.web.bind.annotation.RequestParam;
1618
import org.springframework.web.bind.annotation.RestController;
19+
import org.springframework.web.server.ResponseStatusException;
1720
import org.springframework.web.server.ServerWebExchange;
1821
import reactor.core.publisher.Mono;
1922

@@ -36,16 +39,55 @@ public class GraphQLController {
3639

3740
@RequestMapping(value = "${graphql.url:graphql}",
3841
method = RequestMethod.POST,
39-
consumes = MediaType.APPLICATION_JSON_VALUE,
4042
produces = MediaType.APPLICATION_JSON_VALUE)
41-
public Object graphqlPOST(@RequestBody GraphQLRequestBody body,
42-
ServerWebExchange serverWebExchange) {
43-
String query = body.getQuery();
44-
if (query == null) {
45-
query = "";
43+
public Object graphqlPOST(
44+
@RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType,
45+
@RequestParam(value = "query", required = false) String query,
46+
@RequestParam(value = "operationName", required = false) String operationName,
47+
@RequestParam(value = "variables", required = false) String variablesJson,
48+
@RequestBody(required = false) String body,
49+
ServerWebExchange serverWebExchange) throws IOException {
50+
51+
if (body == null) {
52+
body = "";
4653
}
47-
Mono<ExecutionResult> executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, body.getOperationName(), body.getVariables()), serverWebExchange);
48-
return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse());
54+
55+
// https://graphql.org/learn/serving-over-http/#post-request
56+
//
57+
// A standard GraphQL POST request should use the application/json content type,
58+
// and include a JSON-encoded body of the following form:
59+
//
60+
// {
61+
// "query": "...",
62+
// "operationName": "...",
63+
// "variables": { "myVariable": "someValue", ... }
64+
// }
65+
66+
if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
67+
GraphQLRequestBody request = objectMapper.readValue(body, GraphQLRequestBody.class);
68+
if (request.getQuery() == null) {
69+
request.setQuery("");
70+
}
71+
return executeRequest(request.getQuery(), request.getOperationName(), request.getVariables(), serverWebExchange);
72+
}
73+
74+
// In addition to the above, we recommend supporting two additional cases:
75+
//
76+
// * If the "query" query string parameter is present (as in the GET example above),
77+
// it should be parsed and handled in the same way as the HTTP GET case.
78+
79+
if (query != null) {
80+
return executeRequest(query, operationName, convertVariablesJson(variablesJson), serverWebExchange);
81+
}
82+
83+
// * If the "application/graphql" Content-Type header is present,
84+
// treat the HTTP POST body contents as the GraphQL query string.
85+
86+
if ("application/graphql".equals(contentType)) {
87+
return executeRequest(body, null, null, serverWebExchange);
88+
}
89+
90+
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Could not process GraphQL request");
4991
}
5092

5193
@RequestMapping(value = "${graphql.url:graphql}",
@@ -55,10 +97,28 @@ public Object graphqlGET(
5597
@RequestParam("query") String query,
5698
@RequestParam(value = "operationName", required = false) String operationName,
5799
@RequestParam(value = "variables", required = false) String variablesJson,
58-
ServerWebExchange serverWebExchange
59-
) {
60-
Mono<ExecutionResult> executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, operationName, convertVariablesJson(variablesJson)), serverWebExchange);
61-
return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse());
100+
ServerWebExchange serverWebExchange) {
101+
102+
// https://graphql.org/learn/serving-over-http/#get-request
103+
//
104+
// When receiving an HTTP GET request, the GraphQL query should be specified in the "query" query string.
105+
// For example, if we wanted to execute the following GraphQL query:
106+
//
107+
// {
108+
// me {
109+
// name
110+
// }
111+
// }
112+
//
113+
// This request could be sent via an HTTP GET like so:
114+
//
115+
// http://myapi/graphql?query={me{name}}
116+
//
117+
// Query variables can be sent as a JSON-encoded string in an additional query parameter called "variables".
118+
// If the query contains several named operations,
119+
// an "operationName" query parameter can be used to control which one should be executed.
120+
121+
return executeRequest(query, operationName, convertVariablesJson(variablesJson), serverWebExchange);
62122
}
63123

64124
private Map<String, Object> convertVariablesJson(String jsonMap) {
@@ -71,5 +131,14 @@ private Map<String, Object> convertVariablesJson(String jsonMap) {
71131

72132
}
73133

134+
private Object executeRequest(
135+
String query,
136+
String operationName,
137+
Map<String, Object> variables,
138+
ServerWebExchange serverWebExchange) {
139+
GraphQLInvocationData invocationData = new GraphQLInvocationData(query, operationName, variables);
140+
Mono<ExecutionResult> executionResult = graphQLInvocation.invoke(invocationData, serverWebExchange);
141+
return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse());
142+
}
74143

75144
}

graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DifferentUrlGraphQLControllerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public void testDifferentUrl() throws Exception {
5858
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
5959

6060
client.post().uri("/otherUrl")
61+
.contentType(MediaType.APPLICATION_JSON)
6162
.body(Mono.just(request), Map.class)
6263
.accept(MediaType.APPLICATION_JSON_UTF8)
6364
.exchange()
@@ -70,4 +71,4 @@ public void testDifferentUrl() throws Exception {
7071
assertThat(captor.getValue().getQuery(), is(query));
7172
}
7273

73-
}
74+
}

graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/GraphQLControllerTest.java

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public void testPostRequest() throws Exception {
6464
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
6565

6666
client.post().uri("/graphql")
67+
.contentType(MediaType.APPLICATION_JSON)
6768
.body(Mono.just(request), Map.class)
6869
.accept(MediaType.APPLICATION_JSON_UTF8)
6970
.exchange()
@@ -92,6 +93,7 @@ public void testSimplePostRequest() throws Exception {
9293
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
9394

9495
client.post().uri("/graphql")
96+
.contentType(MediaType.APPLICATION_JSON)
9597
.body(Mono.just(request), Map.class)
9698
.accept(MediaType.APPLICATION_JSON_UTF8)
9799
.exchange()
@@ -104,6 +106,92 @@ public void testSimplePostRequest() throws Exception {
104106
assertThat(captor.getValue().getQuery(), is(query));
105107
}
106108

109+
@Test
110+
public void testQueryParamPostRequest() throws Exception {
111+
String variablesJson = "{\"variable\":\"variableValue\"}";
112+
String variablesValue = URLEncoder.encode(variablesJson, "UTF-8");
113+
String query = "query myQuery {foo}";
114+
String queryString = URLEncoder.encode(query, "UTF-8");
115+
String operationName = "myQuery";
116+
117+
ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult()
118+
.data("bar")
119+
.build();
120+
CompletableFuture cf = CompletableFuture.completedFuture(executionResult);
121+
ArgumentCaptor<ExecutionInput> captor = ArgumentCaptor.forClass(ExecutionInput.class);
122+
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
123+
124+
client.post().uri(uriBuilder -> uriBuilder.path("/graphql")
125+
.queryParam("variables", variablesValue)
126+
.queryParam("query", queryString)
127+
.queryParam("operationName", operationName)
128+
.build(variablesJson, queryString))
129+
.accept(MediaType.APPLICATION_JSON_UTF8)
130+
.exchange()
131+
.expectStatus().isOk()
132+
.expectBody()
133+
.jsonPath("data").isEqualTo("bar");
134+
135+
assertThat(captor.getAllValues().size(), is(1));
136+
137+
Map<String, Object> variables = new LinkedHashMap<>();
138+
variables.put("variable", "variableValue");
139+
assertThat(captor.getValue().getQuery(), is(query));
140+
assertThat(captor.getValue().getVariables(), is(variables));
141+
assertThat(captor.getValue().getOperationName(), is(operationName));
142+
}
143+
144+
@Test
145+
public void testSimpleQueryParamPostRequest() throws Exception {
146+
String query = "{foo}";
147+
String queryString = URLEncoder.encode(query, "UTF-8");
148+
149+
ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult()
150+
.data("bar")
151+
.build();
152+
CompletableFuture cf = CompletableFuture.completedFuture(executionResult);
153+
ArgumentCaptor<ExecutionInput> captor = ArgumentCaptor.forClass(ExecutionInput.class);
154+
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
155+
156+
client.post().uri(uriBuilder -> uriBuilder.path("/graphql")
157+
.queryParam("query", queryString)
158+
.build())
159+
.accept(MediaType.APPLICATION_JSON_UTF8)
160+
.exchange()
161+
.expectStatus().isOk()
162+
.expectBody()
163+
.jsonPath("data").isEqualTo("bar");
164+
165+
166+
assertThat(captor.getAllValues().size(), is(1));
167+
168+
assertThat(captor.getValue().getQuery(), is(query));
169+
}
170+
171+
@Test
172+
public void testApplicationGraphqlPostRequest() throws Exception {
173+
String query = "{foo}";
174+
175+
ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult()
176+
.data("bar")
177+
.build();
178+
CompletableFuture cf = CompletableFuture.completedFuture(executionResult);
179+
ArgumentCaptor<ExecutionInput> captor = ArgumentCaptor.forClass(ExecutionInput.class);
180+
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
181+
182+
client.post().uri("/graphql")
183+
.contentType(new MediaType("application", "graphql"))
184+
.body(Mono.just(query), String.class)
185+
.accept(MediaType.APPLICATION_JSON_UTF8)
186+
.exchange()
187+
.expectStatus().isOk()
188+
.expectBody()
189+
.jsonPath("data").isEqualTo("bar");
190+
191+
assertThat(captor.getAllValues().size(), is(1));
192+
193+
assertThat(captor.getValue().getQuery(), is(query));
194+
}
107195

108196
@Test
109197
public void testGetRequest() throws Exception {
@@ -165,4 +253,4 @@ public void testSimpleGetRequest() throws Exception {
165253
assertThat(captor.getValue().getQuery(), is(query));
166254
}
167255

168-
}
256+
}

0 commit comments

Comments
 (0)