Skip to content

Commit 65a623f

Browse files
committed
feat: support extensions parameter
resolve #104
1 parent 85fd2a8 commit 65a623f

File tree

8 files changed

+133
-47
lines changed

8 files changed

+133
-47
lines changed

graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/MvcAutoConfiguration.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.leangen.graphql.spqr.spring.autoconfigure;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import graphql.GraphQL;
45
import graphql.schema.GraphQLSchema;
56
import io.leangen.graphql.spqr.spring.web.GraphQLController;
@@ -43,8 +44,8 @@ public GraphQLMvcExecutor defaultExecutor(MvcContextFactory contextFactory, Spqr
4344
@ConditionalOnProperty(name = "graphql.spqr.http.enabled", havingValue = "true", matchIfMissing = true)
4445
@ConditionalOnMissingBean(GraphQLController.class)
4546
@ConditionalOnBean(GraphQLSchema.class)
46-
public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLMvcExecutor executor) {
47-
return new DefaultGraphQLController(graphQL, executor);
47+
public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLMvcExecutor executor, ObjectMapper objectMapper) {
48+
return new DefaultGraphQLController(graphQL, executor, objectMapper);
4849
}
4950

5051
@Bean

graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/ReactiveAutoConfiguration.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.leangen.graphql.spqr.spring.autoconfigure;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import graphql.GraphQL;
45
import graphql.schema.GraphQLSchema;
56
import io.leangen.graphql.module.Module;
@@ -47,8 +48,8 @@ public GraphQLReactiveExecutor graphQLExecutor(ReactiveContextFactory contextFac
4748
@ConditionalOnProperty(name = "graphql.spqr.http.enabled", havingValue = "true", matchIfMissing = true)
4849
@ConditionalOnMissingBean(GraphQLController.class)
4950
@ConditionalOnBean(GraphQLSchema.class)
50-
public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor) {
51-
return new DefaultGraphQLController(graphQL, executor);
51+
public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor, ObjectMapper objectMapper) {
52+
return new DefaultGraphQLController(graphQL, executor,objectMapper);
5253
}
5354

5455
@Bean

graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/GraphQLController.java

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
11
package io.leangen.graphql.spqr.spring.web;
22

3-
import graphql.GraphQL;
4-
import io.leangen.graphql.spqr.spring.web.dto.ExecutorParams;
5-
import io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest;
6-
import io.leangen.graphql.spqr.spring.web.dto.TransportType;
7-
import io.leangen.graphql.util.Utils;
3+
import java.io.IOException;
4+
import java.util.Collections;
5+
import java.util.Map;
6+
87
import org.springframework.http.MediaType;
98
import org.springframework.web.bind.annotation.GetMapping;
109
import org.springframework.web.bind.annotation.PostMapping;
1110
import org.springframework.web.bind.annotation.RequestBody;
12-
import org.springframework.web.bind.annotation.RequestMapping;
13-
import org.springframework.web.bind.annotation.RequestMethod;
1411
import org.springframework.web.bind.annotation.RequestParam;
1512
import org.springframework.web.bind.annotation.RestController;
1613

17-
import java.util.Map;
14+
import com.fasterxml.jackson.core.type.TypeReference;
15+
import com.fasterxml.jackson.databind.ObjectMapper;
16+
17+
import graphql.GraphQL;
18+
import io.leangen.graphql.spqr.spring.web.dto.ExecutorParams;
19+
import io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest;
20+
import io.leangen.graphql.spqr.spring.web.dto.TransportType;
21+
import io.leangen.graphql.util.Utils;
1822

1923
@RestController
2024
public abstract class GraphQLController<R> {
2125

2226
protected final GraphQL graphQL;
2327
protected final GraphQLExecutor<R> executor;
28+
protected final ObjectMapper objectMapper;
2429

25-
public GraphQLController(GraphQL graphQL, GraphQLExecutor<R> executor) {
30+
public GraphQLController(GraphQL graphQL, GraphQLExecutor<R> executor, ObjectMapper objectMapper) {
2631
this.graphQL = graphQL;
2732
this.executor = executor;
33+
this.objectMapper = objectMapper;
2834
}
2935

3036
@PostMapping(
@@ -53,7 +59,8 @@ public Object jsonPost(GraphQLRequest requestBody, GraphQLRequest requestParams,
5359
String query = Utils.isNotEmpty(requestParams.getQuery()) ? requestParams.getQuery() : requestBody.getQuery();
5460
String operationName = Utils.isNotEmpty(requestParams.getOperationName()) ? requestParams.getOperationName() : requestBody.getOperationName();
5561
Map<String, Object> variables = requestParams.getVariables().isEmpty() ? requestBody.getVariables() : requestParams.getVariables();
56-
ExecutorParams<R> params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, variables), request, transportType);
62+
Map<String, Object> extensions = requestParams.getExtensions().isEmpty() ? requestBody.getExtensions() : requestParams.getExtensions();
63+
ExecutorParams<R> params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, variables, extensions), request, transportType);
5764
return executor.execute(graphQL, params);
5865
}
5966

@@ -66,13 +73,12 @@ public Object executeGraphQLPost(@RequestBody String queryBody,
6673
GraphQLRequest originalReq,
6774
R request) {
6875
String query = Utils.isNotEmpty(originalReq.getQuery()) ? originalReq.getQuery() : queryBody;
69-
GraphQLRequest remappedReq = new GraphQLRequest(originalReq.getId(), query, originalReq.getOperationName(), originalReq.getVariables());
76+
GraphQLRequest remappedReq = new GraphQLRequest(originalReq.getId(), query, originalReq.getOperationName(), originalReq.getVariables(), originalReq.getExtensions());
7077
ExecutorParams<R> params = new ExecutorParams<>(remappedReq, request, TransportType.HTTP);
7178
return executor.execute(graphQL, params);
7279
}
7380

74-
@RequestMapping(
75-
method = RequestMethod.POST,
81+
@PostMapping(
7682
value = "${graphql.spqr.http.endpoint:/graphql}",
7783
consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE, "application/x-www-form-urlencoded;charset=UTF-8"},
7884
produces = MediaType.APPLICATION_JSON_VALUE
@@ -88,7 +94,7 @@ public Object executeFormPost(@RequestParam Map<String, String> queryParams,
8894
String id = Utils.isNotEmpty(idParam) ? idParam : graphQLRequest.getId();
8995
String query = Utils.isNotEmpty(queryParam) ? queryParam : graphQLRequest.getQuery();
9096
String operationName = Utils.isEmpty(operationNameParam) ? graphQLRequest.getOperationName() : operationNameParam;
91-
ExecutorParams<R> params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, graphQLRequest.getVariables()), request, TransportType.HTTP);
97+
ExecutorParams<R> params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, graphQLRequest.getVariables(), graphQLRequest.getExtensions()), request, TransportType.HTTP);
9298

9399
return executor.execute(graphQL, params);
94100
}
@@ -98,20 +104,41 @@ public Object executeFormPost(@RequestParam Map<String, String> queryParams,
98104
produces = MediaType.APPLICATION_JSON_VALUE,
99105
headers = { "Connection!=Upgrade", "Connection!=keep-alive, Upgrade" }
100106
)
101-
public Object executeGet(GraphQLRequest graphQLRequest, R request) {
102-
return get(graphQLRequest, request, TransportType.HTTP);
107+
public Object executeGet(String id,
108+
String query,
109+
String operationName,
110+
String variables,
111+
String extensions,
112+
R request) {
113+
return get(new GraphQLRequest(id, query, operationName, parseAsMap(variables), parseAsMap(extensions)), request, TransportType.HTTP);
114+
}
115+
116+
private Object get(GraphQLRequest graphQLRequest, R request, TransportType transportType) {
117+
return executor.execute(graphQL, new ExecutorParams<>(graphQLRequest, request, transportType));
103118
}
104119

105120
@GetMapping(
106121
value = "${graphql.spqr.http.endpoint:/graphql}",
107122
produces = MediaType.TEXT_EVENT_STREAM_VALUE,
108123
headers = { "Connection!=Upgrade", "Connection!=keep-alive, Upgrade" }
109124
)
110-
public Object executeGetEventStream(GraphQLRequest graphQLRequest, R request) {
111-
return get(graphQLRequest, request, TransportType.HTTP_EVENT_STREAM);
125+
public Object executeGetEventStream(String id,
126+
String query,
127+
String operationName,
128+
String variables,
129+
String extensions,
130+
R request) {
131+
return get(new GraphQLRequest(id, query, operationName, parseAsMap(variables), parseAsMap(extensions)), request, TransportType.HTTP_EVENT_STREAM);
112132
}
113133

114-
private Object get(GraphQLRequest graphQLRequest, R request, TransportType transportType) {
115-
return executor.execute(graphQL, new ExecutorParams<>(graphQLRequest, request, transportType));
134+
private Map<String, Object> parseAsMap(String str) {
135+
if (str == null || str.trim().isEmpty()) {
136+
return Collections.emptyMap();
137+
}
138+
try {
139+
return objectMapper.readValue(str, new TypeReference<Map<String, Object>>() {});
140+
} catch (IOException e) {
141+
throw new IllegalArgumentException("failed to parse: " + str);
142+
}
116143
}
117144
}

graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/dto/GraphQLRequest.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ public class GraphQLRequest {
1414
private final String query;
1515
private final String operationName;
1616
private final Map<String, Object> variables;
17+
private final Map<String, Object> extensions;
1718

18-
@JsonCreator
19+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
1920
public GraphQLRequest(@JsonProperty("id") String id,
2021
@JsonProperty("query") String query,
2122
@JsonProperty("operationName") String operationName,
22-
@JsonProperty("variables") Map<String, Object> variables) {
23+
@JsonProperty("variables") Map<String, Object> variables,
24+
@JsonProperty("extensions") Map<String, Object> extensions) {
2325
this.id = id;
24-
this.query = query;
26+
this.query = query == null && extensions != null ? "" : query;
2527
this.operationName = operationName;
2628
this.variables = variables != null ? variables : Collections.emptyMap();
29+
this.extensions = extensions != null ? extensions : Collections.emptyMap();
2730
}
2831

2932
public String getId() {
@@ -41,4 +44,7 @@ public String getOperationName() {
4144
public Map<String, Object> getVariables() {
4245
return variables;
4346
}
47+
48+
public Map<String, Object> getExtensions() { return extensions; }
49+
4450
}

graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/mvc/DefaultGraphQLController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.leangen.graphql.spqr.spring.web.mvc;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import graphql.GraphQL;
45
import io.leangen.graphql.spqr.spring.web.GraphQLController;
56
import org.springframework.beans.factory.annotation.Autowired;
@@ -12,7 +13,7 @@
1213
public class DefaultGraphQLController extends GraphQLController<NativeWebRequest> {
1314

1415
@Autowired
15-
public DefaultGraphQLController(GraphQL graphQL, GraphQLMvcExecutor executor) {
16-
super(graphQL, executor);
16+
public DefaultGraphQLController(GraphQL graphQL, GraphQLMvcExecutor executor, ObjectMapper objectMapper) {
17+
super(graphQL, executor, objectMapper);
1718
}
1819
}

graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/reactive/DefaultGraphQLController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.leangen.graphql.spqr.spring.web.reactive;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import graphql.GraphQL;
45
import io.leangen.graphql.spqr.spring.web.GraphQLController;
56
import org.springframework.beans.factory.annotation.Autowired;
@@ -12,7 +13,7 @@
1213
public class DefaultGraphQLController extends GraphQLController<ServerWebExchange> {
1314

1415
@Autowired
15-
public DefaultGraphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor) {
16-
super(graphQL, executor);
16+
public DefaultGraphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor, ObjectMapper objectMapper) {
17+
super(graphQL, executor, objectMapper);
1718
}
1819
}

graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package io.leangen.graphql.spqr.spring.web;
22

3-
import io.leangen.graphql.spqr.spring.autoconfigure.BaseAutoConfiguration;
4-
import io.leangen.graphql.spqr.spring.autoconfigure.MvcAutoConfiguration;
5-
import io.leangen.graphql.spqr.spring.autoconfigure.SpringDataAutoConfiguration;
6-
import io.leangen.graphql.spqr.spring.test.ResolverBuilder_TestConfig;
3+
import static org.hamcrest.Matchers.containsString;
4+
import static org.hamcrest.Matchers.equalToCompressingWhiteSpace;
5+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
7+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
8+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
9+
10+
import java.net.URLEncoder;
11+
import java.nio.charset.StandardCharsets;
12+
713
import org.junit.Test;
814
import org.junit.runner.RunWith;
915
import org.springframework.beans.factory.annotation.Autowired;
@@ -15,15 +21,10 @@
1521
import org.springframework.test.context.junit4.SpringRunner;
1622
import org.springframework.test.web.servlet.MockMvc;
1723

18-
import java.net.URLEncoder;
19-
import java.nio.charset.StandardCharsets;
20-
21-
import static org.hamcrest.Matchers.containsString;
22-
import static org.hamcrest.Matchers.equalToCompressingWhiteSpace;
23-
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
24-
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
25-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
26-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
24+
import io.leangen.graphql.spqr.spring.autoconfigure.BaseAutoConfiguration;
25+
import io.leangen.graphql.spqr.spring.autoconfigure.MvcAutoConfiguration;
26+
import io.leangen.graphql.spqr.spring.autoconfigure.SpringDataAutoConfiguration;
27+
import io.leangen.graphql.spqr.spring.test.ResolverBuilder_TestConfig;
2728

2829
@RunWith(SpringRunner.class)
2930
@WebMvcTest
@@ -48,12 +49,22 @@ public void defaultControllerTest_POST_applicationGraphql_noQueryParams() throws
4849
.andExpect(content().string(containsString("Hello world")));
4950
}
5051

52+
@Test
53+
public void defaultControllerTest_POST_applicationJson_persistedQuery() throws Exception {
54+
mockMvc.perform(
55+
post("/"+apiContext)
56+
.contentType(MediaType.APPLICATION_JSON)
57+
.content("{\"variables\":null,\"operationName\":null,\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"fcf31818e50ac3e818ca4bdbc433d6ab73176f0b9d5f9d5ad17e200cdab6fba4\"}}}"))
58+
.andExpect(status().isOk())
59+
.andExpect(content().string(containsString("Invalid Syntax : offending token '<EOF>' at line 1 column 1")));
60+
}
61+
5162
@Test
5263
public void defaultControllerTest_POST_applicationJson_noQueryParams() throws Exception {
5364
mockMvc.perform(
5465
post("/"+apiContext)
5566
.contentType(MediaType.APPLICATION_JSON)
56-
.content("{\"query\":\"{greetingFromBeanSource_wiredAsComponent_byAnnotation}\",\"variables\":null,\"operationName\":null}"))
67+
.content("{\"query\":\"{greetingFromBeanSource_wiredAsComponent_byAnnotation}\",\"variables\":null,\"operationName\":null,\"extensions\":null}"))
5768
.andExpect(status().isOk())
5869
.andExpect(content().string(containsString("Hello world")));
5970
}
@@ -77,6 +88,15 @@ public void defaultControllerTest_GET() throws Exception {
7788
.andExpect(content().string(containsString("Hello world")));
7889
}
7990

91+
@Test
92+
public void defaultControllerTest_GET_persistedQuery() throws Exception {
93+
mockMvc.perform(
94+
get("/"+apiContext)
95+
.param("extensions", "{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"4b758938f2d00323147290e3b0d041e6a0952e2c694ab2c0ea7212ca08f337b3\"}}"))
96+
.andExpect(status().isOk())
97+
.andExpect(content().string(containsString("Invalid Syntax : offending token '<EOF>' at line 1 column 1")));
98+
}
99+
80100
@Test
81101
public void defaultControllerTest_POST_applicationGraphql_INVALID() throws Exception {
82102
mockMvc.perform(
@@ -103,7 +123,7 @@ public void defaultControllerTest_POST_applicationJson_INVALID() throws Exceptio
103123
mockMvc.perform(
104124
post("/"+apiContext)
105125
.contentType(MediaType.APPLICATION_JSON)
106-
.content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null}"))
126+
.content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null,\"extensions\":null}"))
107127
.andExpect(status().isOk())
108128
.andExpect(content().string(containsString("FieldUndefined: Field 'INVALID_QUERY'")));
109129
}
@@ -114,7 +134,7 @@ public void defaultControllerTest_POST_applicationJson_overridingQueryParams() t
114134
post("/"+apiContext)
115135
.param("query","{greetingFromBeanSource_wiredAsComponent_byAnnotation}")
116136
.contentType(MediaType.APPLICATION_JSON)
117-
.content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null}"))
137+
.content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null,\"extensions\":null}"))
118138
.andExpect(status().isOk())
119139
.andExpect(content().string(containsString("Hello world")));
120140
}

graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/reactive/GraphQLReactiveControllerTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.web.reactive.function.BodyInserters;
2020

2121
import java.net.URI;
22+
import java.util.Map;
2223

2324
import static org.hamcrest.Matchers.containsString;
2425
import static org.hamcrest.MatcherAssert.assertThat;
@@ -44,6 +45,34 @@ public void setUp() {
4445
webTestClient = WebTestClient.bindToApplicationContext(context).build();
4546
}
4647

48+
@Test
49+
public void defaultControllerTest_GET_mono() {
50+
webTestClient.get()
51+
.uri(uriBuilder -> uriBuilder
52+
.path("/" + apiContext)
53+
.queryParam("query", "{query}")
54+
.build("{greetingFromAnnotatedSourceReactive_mono}"))
55+
.accept(MediaType.TEXT_EVENT_STREAM)
56+
.exchange()
57+
.expectStatus().isOk()
58+
.expectBody(String.class)
59+
.consumeWith(c -> assertThat("", c.getResponseBody(), containsString("Hello world !")));
60+
}
61+
62+
@Test
63+
public void defaultControllerTest_GET_persistedQuery_mono() {
64+
webTestClient.get()
65+
.uri(uriBuilder -> uriBuilder
66+
.path("/" + apiContext)
67+
.queryParam("extensions", "{persisted}")
68+
.build("{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"4b758938f2d00323147290e3b0d041e6a0952e2c694ab2c0ea7212ca08f337b3\"}}"))
69+
.accept(MediaType.TEXT_EVENT_STREAM)
70+
.exchange()
71+
.expectStatus().isOk()
72+
.expectBody(String.class)
73+
.consumeWith(c -> assertThat("", c.getResponseBody(), containsString("Invalid Syntax : offending token '<EOF>' at line 1 column 1\"")));
74+
}
75+
4776
@Test
4877
public void defaultControllerTest_POST_formUrlEncoded_mono() {
4978
LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();

0 commit comments

Comments
 (0)