diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/MvcAutoConfiguration.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/MvcAutoConfiguration.java index 0ebba5f..b3de175 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/MvcAutoConfiguration.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/MvcAutoConfiguration.java @@ -1,5 +1,6 @@ package io.leangen.graphql.spqr.spring.autoconfigure; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.GraphQL; import graphql.schema.GraphQLSchema; import io.leangen.graphql.spqr.spring.web.GraphQLController; @@ -43,8 +44,8 @@ public GraphQLMvcExecutor defaultExecutor(MvcContextFactory contextFactory, Spqr @ConditionalOnProperty(name = "graphql.spqr.http.enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnMissingBean(GraphQLController.class) @ConditionalOnBean(GraphQLSchema.class) - public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLMvcExecutor executor) { - return new DefaultGraphQLController(graphQL, executor); + public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLMvcExecutor executor, ObjectMapper objectMapper) { + return new DefaultGraphQLController(graphQL, executor, objectMapper); } @Bean diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/ReactiveAutoConfiguration.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/ReactiveAutoConfiguration.java index 1732afe..0bf0149 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/ReactiveAutoConfiguration.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/autoconfigure/ReactiveAutoConfiguration.java @@ -1,5 +1,6 @@ package io.leangen.graphql.spqr.spring.autoconfigure; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.GraphQL; import graphql.schema.GraphQLSchema; import io.leangen.graphql.module.Module; @@ -47,8 +48,8 @@ public GraphQLReactiveExecutor graphQLExecutor(ReactiveContextFactory contextFac @ConditionalOnProperty(name = "graphql.spqr.http.enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnMissingBean(GraphQLController.class) @ConditionalOnBean(GraphQLSchema.class) - public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor) { - return new DefaultGraphQLController(graphQL, executor); + public DefaultGraphQLController graphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor, ObjectMapper objectMapper) { + return new DefaultGraphQLController(graphQL, executor,objectMapper); } @Bean diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/GraphQLController.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/GraphQLController.java index 5e5d6e1..eaebce5 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/GraphQLController.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/GraphQLController.java @@ -1,30 +1,36 @@ package io.leangen.graphql.spqr.spring.web; -import graphql.GraphQL; -import io.leangen.graphql.spqr.spring.web.dto.ExecutorParams; -import io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest; -import io.leangen.graphql.spqr.spring.web.dto.TransportType; -import io.leangen.graphql.util.Utils; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.Map; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import graphql.GraphQL; +import io.leangen.graphql.spqr.spring.web.dto.ExecutorParams; +import io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest; +import io.leangen.graphql.spqr.spring.web.dto.TransportType; +import io.leangen.graphql.util.Utils; @RestController public abstract class GraphQLController { protected final GraphQL graphQL; protected final GraphQLExecutor executor; + protected final ObjectMapper objectMapper; - public GraphQLController(GraphQL graphQL, GraphQLExecutor executor) { + public GraphQLController(GraphQL graphQL, GraphQLExecutor executor, final ObjectMapper objectMapper) { this.graphQL = graphQL; this.executor = executor; + this.objectMapper = objectMapper; } @PostMapping( @@ -53,7 +59,8 @@ public Object jsonPost(GraphQLRequest requestBody, GraphQLRequest requestParams, String query = Utils.isNotEmpty(requestParams.getQuery()) ? requestParams.getQuery() : requestBody.getQuery(); String operationName = Utils.isNotEmpty(requestParams.getOperationName()) ? requestParams.getOperationName() : requestBody.getOperationName(); Map variables = requestParams.getVariables().isEmpty() ? requestBody.getVariables() : requestParams.getVariables(); - ExecutorParams params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, variables), request, transportType); + Map extensions = requestParams.getExtensions().isEmpty() ? requestBody.getExtensions() : requestParams.getExtensions(); + ExecutorParams params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, variables, extensions), request, transportType); return executor.execute(graphQL, params); } @@ -66,13 +73,12 @@ public Object executeGraphQLPost(@RequestBody String queryBody, GraphQLRequest originalReq, R request) { String query = Utils.isNotEmpty(originalReq.getQuery()) ? originalReq.getQuery() : queryBody; - GraphQLRequest remappedReq = new GraphQLRequest(originalReq.getId(), query, originalReq.getOperationName(), originalReq.getVariables()); + GraphQLRequest remappedReq = new GraphQLRequest(originalReq.getId(), query, originalReq.getOperationName(), originalReq.getVariables(), originalReq.getExtensions()); ExecutorParams params = new ExecutorParams<>(remappedReq, request, TransportType.HTTP); return executor.execute(graphQL, params); } - @RequestMapping( - method = RequestMethod.POST, + @PostMapping( value = "${graphql.spqr.http.endpoint:/graphql}", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE, "application/x-www-form-urlencoded;charset=UTF-8"}, produces = MediaType.APPLICATION_JSON_VALUE @@ -88,7 +94,7 @@ public Object executeFormPost(@RequestParam Map queryParams, String id = Utils.isNotEmpty(idParam) ? idParam : graphQLRequest.getId(); String query = Utils.isNotEmpty(queryParam) ? queryParam : graphQLRequest.getQuery(); String operationName = Utils.isEmpty(operationNameParam) ? graphQLRequest.getOperationName() : operationNameParam; - ExecutorParams params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, graphQLRequest.getVariables()), request, TransportType.HTTP); + ExecutorParams params = new ExecutorParams<>(new GraphQLRequest(id, query, operationName, graphQLRequest.getVariables(), graphQLRequest.getExtensions()), request, TransportType.HTTP); return executor.execute(graphQL, params); } @@ -98,8 +104,17 @@ public Object executeFormPost(@RequestParam Map queryParams, produces = MediaType.APPLICATION_JSON_VALUE, headers = { "Connection!=Upgrade", "Connection!=keep-alive, Upgrade" } ) - public Object executeGet(GraphQLRequest graphQLRequest, R request) { - return get(graphQLRequest, request, TransportType.HTTP); + public Object executeGet(String id, + String query, + String operationName, + String variables, + String extensions, + R request) { + return get(new GraphQLRequest(id, query, operationName, parseAsMap(variables), parseAsMap(extensions)), request, TransportType.HTTP); + } + + private Object get(GraphQLRequest graphQLRequest, R request, TransportType transportType) { + return executor.execute(graphQL, new ExecutorParams<>(graphQLRequest, request, transportType)); } @GetMapping( @@ -107,11 +122,23 @@ public Object executeGet(GraphQLRequest graphQLRequest, R request) { produces = MediaType.TEXT_EVENT_STREAM_VALUE, headers = { "Connection!=Upgrade", "Connection!=keep-alive, Upgrade" } ) - public Object executeGetEventStream(GraphQLRequest graphQLRequest, R request) { - return get(graphQLRequest, request, TransportType.HTTP_EVENT_STREAM); + public Object executeGetEventStream(String id, + String query, + String operationName, + String variables, + String extensions, + R request) { + return get(new GraphQLRequest(id, query, operationName, parseAsMap(variables), parseAsMap(extensions)), request, TransportType.HTTP_EVENT_STREAM); } - private Object get(GraphQLRequest graphQLRequest, R request, TransportType transportType) { - return executor.execute(graphQL, new ExecutorParams<>(graphQLRequest, request, transportType)); + private Map parseAsMap(String str) { + if (Utils.isEmpty(str)) { + return Collections.emptyMap(); + } + try { + return objectMapper.readValue(str, new TypeReference>() {}); + } catch (IOException e) { + throw new IllegalArgumentException("failed to parse: " + str); + } } } diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/dto/GraphQLRequest.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/dto/GraphQLRequest.java index abaed52..d3b2e57 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/dto/GraphQLRequest.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/dto/GraphQLRequest.java @@ -14,16 +14,19 @@ public class GraphQLRequest { private final String query; private final String operationName; private final Map variables; + private final Map extensions; - @JsonCreator + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public GraphQLRequest(@JsonProperty("id") String id, @JsonProperty("query") String query, @JsonProperty("operationName") String operationName, - @JsonProperty("variables") Map variables) { + @JsonProperty("variables") Map variables, + @JsonProperty("extensions") Map extensions) { this.id = id; - this.query = query; + this.query = query == null && extensions != null ? "" : query; this.operationName = operationName; this.variables = variables != null ? variables : Collections.emptyMap(); + this.extensions = extensions != null ? extensions : Collections.emptyMap(); } public String getId() { @@ -41,4 +44,7 @@ public String getOperationName() { public Map getVariables() { return variables; } + + public Map getExtensions() { return extensions; } + } diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/mvc/DefaultGraphQLController.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/mvc/DefaultGraphQLController.java index 68053e8..47d3af8 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/mvc/DefaultGraphQLController.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/mvc/DefaultGraphQLController.java @@ -1,5 +1,6 @@ package io.leangen.graphql.spqr.spring.web.mvc; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.GraphQL; import io.leangen.graphql.spqr.spring.web.GraphQLController; import org.springframework.beans.factory.annotation.Autowired; @@ -12,7 +13,7 @@ public class DefaultGraphQLController extends GraphQLController { @Autowired - public DefaultGraphQLController(GraphQL graphQL, GraphQLMvcExecutor executor) { - super(graphQL, executor); + public DefaultGraphQLController(GraphQL graphQL, GraphQLMvcExecutor executor, ObjectMapper objectMapper) { + super(graphQL, executor, objectMapper); } } diff --git a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/reactive/DefaultGraphQLController.java b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/reactive/DefaultGraphQLController.java index e6b2f30..da9ecad 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/reactive/DefaultGraphQLController.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/main/java/io/leangen/graphql/spqr/spring/web/reactive/DefaultGraphQLController.java @@ -1,5 +1,6 @@ package io.leangen.graphql.spqr.spring.web.reactive; +import com.fasterxml.jackson.databind.ObjectMapper; import graphql.GraphQL; import io.leangen.graphql.spqr.spring.web.GraphQLController; import org.springframework.beans.factory.annotation.Autowired; @@ -12,7 +13,7 @@ public class DefaultGraphQLController extends GraphQLController { @Autowired - public DefaultGraphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor) { - super(graphQL, executor); + public DefaultGraphQLController(GraphQL graphQL, GraphQLReactiveExecutor executor, ObjectMapper objectMapper) { + super(graphQL, executor, objectMapper); } } diff --git a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java index 3c26c17..2d8cd7a 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/GraphQLControllerTest.java @@ -1,9 +1,15 @@ package io.leangen.graphql.spqr.spring.web; -import io.leangen.graphql.spqr.spring.autoconfigure.BaseAutoConfiguration; -import io.leangen.graphql.spqr.spring.autoconfigure.MvcAutoConfiguration; -import io.leangen.graphql.spqr.spring.autoconfigure.SpringDataAutoConfiguration; -import io.leangen.graphql.spqr.spring.test.ResolverBuilder_TestConfig; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalToCompressingWhiteSpace; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -15,15 +21,10 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalToCompressingWhiteSpace; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import io.leangen.graphql.spqr.spring.autoconfigure.BaseAutoConfiguration; +import io.leangen.graphql.spqr.spring.autoconfigure.MvcAutoConfiguration; +import io.leangen.graphql.spqr.spring.autoconfigure.SpringDataAutoConfiguration; +import io.leangen.graphql.spqr.spring.test.ResolverBuilder_TestConfig; @RunWith(SpringRunner.class) @WebMvcTest @@ -48,12 +49,22 @@ public void defaultControllerTest_POST_applicationGraphql_noQueryParams() throws .andExpect(content().string(containsString("Hello world"))); } + @Test + public void defaultControllerTest_POST_applicationJson_persistedQuery() throws Exception { + mockMvc.perform( + post("/"+apiContext) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"variables\":null,\"operationName\":null,\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"fcf31818e50ac3e818ca4bdbc433d6ab73176f0b9d5f9d5ad17e200cdab6fba4\"}}}")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Invalid Syntax : offending token '' at line 1 column 1"))); + } + @Test public void defaultControllerTest_POST_applicationJson_noQueryParams() throws Exception { mockMvc.perform( post("/"+apiContext) .contentType(MediaType.APPLICATION_JSON) - .content("{\"query\":\"{greetingFromBeanSource_wiredAsComponent_byAnnotation}\",\"variables\":null,\"operationName\":null}")) + .content("{\"query\":\"{greetingFromBeanSource_wiredAsComponent_byAnnotation}\",\"variables\":null,\"operationName\":null,\"extensions\":null}")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Hello world"))); } @@ -77,6 +88,15 @@ public void defaultControllerTest_GET() throws Exception { .andExpect(content().string(containsString("Hello world"))); } + @Test + public void defaultControllerTest_GET_persistedQuery() throws Exception { + mockMvc.perform( + get("/"+apiContext) + .param("extensions", "{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"4b758938f2d00323147290e3b0d041e6a0952e2c694ab2c0ea7212ca08f337b3\"}}")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Invalid Syntax : offending token '' at line 1 column 1"))); + } + @Test public void defaultControllerTest_POST_applicationGraphql_INVALID() throws Exception { mockMvc.perform( @@ -103,7 +123,7 @@ public void defaultControllerTest_POST_applicationJson_INVALID() throws Exceptio mockMvc.perform( post("/"+apiContext) .contentType(MediaType.APPLICATION_JSON) - .content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null}")) + .content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null,\"extensions\":null}")) .andExpect(status().isOk()) .andExpect(content().string(containsString("FieldUndefined: Field 'INVALID_QUERY'"))); } @@ -114,7 +134,7 @@ public void defaultControllerTest_POST_applicationJson_overridingQueryParams() t post("/"+apiContext) .param("query","{greetingFromBeanSource_wiredAsComponent_byAnnotation}") .contentType(MediaType.APPLICATION_JSON) - .content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null}")) + .content("{\"query\":\"{INVALID_QUERY}\",\"variables\":null,\"operationName\":null,\"extensions\":null}")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Hello world"))); } diff --git a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/reactive/GraphQLReactiveControllerTest.java b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/reactive/GraphQLReactiveControllerTest.java index 5369c75..cae311b 100644 --- a/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/reactive/GraphQLReactiveControllerTest.java +++ b/graphql-spqr-spring-boot-autoconfigure/src/test/java/io/leangen/graphql/spqr/spring/web/reactive/GraphQLReactiveControllerTest.java @@ -19,6 +19,7 @@ import org.springframework.web.reactive.function.BodyInserters; import java.net.URI; +import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; @@ -44,6 +45,34 @@ public void setUp() { webTestClient = WebTestClient.bindToApplicationContext(context).build(); } + @Test + public void defaultControllerTest_GET_mono() { + webTestClient.get() + .uri(uriBuilder -> uriBuilder + .path("/" + apiContext) + .queryParam("query", "{query}") + .build("{greetingFromAnnotatedSourceReactive_mono}")) + .accept(MediaType.TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .expectBody(String.class) + .consumeWith(c -> assertThat("", c.getResponseBody(), containsString("Hello world !"))); + } + + @Test + public void defaultControllerTest_GET_persistedQuery_mono() { + webTestClient.get() + .uri(uriBuilder -> uriBuilder + .path("/" + apiContext) + .queryParam("extensions", "{persisted}") + .build("{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"4b758938f2d00323147290e3b0d041e6a0952e2c694ab2c0ea7212ca08f337b3\"}}")) + .accept(MediaType.TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .expectBody(String.class) + .consumeWith(c -> assertThat("", c.getResponseBody(), containsString("Invalid Syntax : offending token '' at line 1 column 1\""))); + } + @Test public void defaultControllerTest_POST_formUrlEncoded_mono() { LinkedMultiValueMap body = new LinkedMultiValueMap<>();