From 0099460155f37fd555f5efedc2c65f8633a83527 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 21 Dec 2021 08:33:11 +0100 Subject: [PATCH] Auto-configure graphiql endpoint Spring GraphQL ships with a static version of the graphiql IDE for exploring and querying GraphQL endpoints. See https://github.com/graphql/graphiql for more information. This commit auto-configures the GraphiQL handler for both MVC and WebFlux and points GraphiQL to the GraphQL HTTP endpoint exposed by the application. This feature is disabled by default and can be switched on with "spring.graphql.graphiql.enabled=true". See gh-29140 --- .../graphql/GraphQlProperties.java | 36 +++++++++++++++++++ .../GraphQlWebFluxAutoConfiguration.java | 6 ++++ .../GraphQlWebMvcAutoConfiguration.java | 6 ++++ .../GraphQlWebFluxAutoConfigurationTests.java | 31 +++++++++++----- .../GraphQlWebMvcAutoConfigurationTests.java | 16 +++++++-- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java index 16cb18749d..c9d7e8bdfa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java @@ -34,8 +34,14 @@ public class GraphQlProperties { */ private String path = "/graphql"; + private final Graphiql graphiql = new Graphiql(); + private final Schema schema = new Schema(); + public Graphiql getGraphiql() { + return this.graphiql; + } + public String getPath() { return this.path; } @@ -107,4 +113,34 @@ public class GraphQlProperties { } + public static class Graphiql { + + /** + * Path to the GraphiQL UI endpoint. + */ + private String path = "/graphiql"; + + /** + * Whether the default GraphiQL UI is enabled. + */ + private boolean enabled = false; + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java index 0c068bbe08..fd55b2d9c2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.graphql.execution.GraphQlSource; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebInterceptor; import org.springframework.graphql.web.webflux.GraphQlHttpHandler; +import org.springframework.graphql.web.webflux.GraphiQlHandler; import org.springframework.graphql.web.webflux.SchemaHandler; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -97,6 +98,11 @@ public class GraphQlWebFluxAutoConfiguration { .POST(graphQLPath, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), handler::handleRequest); + if (properties.getGraphiql().isEnabled()) { + GraphiQlHandler graphiQlHandler = new GraphiQlHandler(graphQLPath); + builder = builder.GET(properties.getGraphiql().getPath(), graphiQlHandler::handleRequest); + } + if (properties.getSchema().getPrinter().isEnabled()) { SchemaHandler schemaHandler = new SchemaHandler(graphQlSource); builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java index 6820c83b16..534b495657 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java @@ -41,6 +41,7 @@ import org.springframework.graphql.execution.ThreadLocalAccessor; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebInterceptor; import org.springframework.graphql.web.webmvc.GraphQlHttpHandler; +import org.springframework.graphql.web.webmvc.GraphiQlHandler; import org.springframework.graphql.web.webmvc.SchemaHandler; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -98,6 +99,11 @@ public class GraphQlWebMvcAutoConfiguration { .POST(graphQLPath, RequestPredicates.contentType(MediaType.APPLICATION_JSON) .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest); + if (properties.getGraphiql().isEnabled()) { + GraphiQlHandler graphiQLHandler = new GraphiQlHandler(graphQLPath); + builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest); + } + if (properties.getSchema().getPrinter().isEnabled()) { SchemaHandler schemaHandler = new SchemaHandler(graphQlSource); builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java index c39d997fc5..e7a05476bf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/reactive/GraphQlWebFluxAutoConfigurationTests.java @@ -47,20 +47,21 @@ import static org.hamcrest.Matchers.containsString; */ class GraphQlWebFluxAutoConfigurationTests { - private static final String BASE_URL = "https://spring.example.org/graphql"; + private static final String BASE_URL = "https://spring.example.org/"; private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class, GraphQlWebFluxAutoConfiguration.class)) - .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class).withPropertyValues( - "spring.main.web-application-type=reactive", "spring.graphql.schema.printer.enabled=true"); + .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) + .withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.graphiql.enabled=true", + "spring.graphql.schema.printer.enabled=true"); @Test void simpleQueryShouldWork() { testWithWebClient((client) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() + client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() .expectBody().jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners"); }); } @@ -69,19 +70,21 @@ class GraphQlWebFluxAutoConfigurationTests { void httpGetQueryShouldBeSupported() { testWithWebClient((client) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - client.get().uri("?query={query}", "{ \"query\": \"" + query + "\"}").exchange().expectStatus() + client.get().uri("/graphql?query={query}", "{ \"query\": \"" + query + "\"}").exchange().expectStatus() .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED).expectHeader().valueEquals("Allow", "POST"); }); } @Test void shouldRejectMissingQuery() { - testWithWebClient((client) -> client.post().uri("").bodyValue("{}").exchange().expectStatus().isBadRequest()); + testWithWebClient( + (client) -> client.post().uri("/graphql").bodyValue("{}").exchange().expectStatus().isBadRequest()); } @Test void shouldRejectQueryWithInvalidJson() { - testWithWebClient((client) -> client.post().uri("").bodyValue(":)").exchange().expectStatus().isBadRequest()); + testWithWebClient( + (client) -> client.post().uri("/graphql").bodyValue(":)").exchange().expectStatus().isBadRequest()); } @Test @@ -89,18 +92,28 @@ class GraphQlWebFluxAutoConfigurationTests { testWithWebClient((client) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() + client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() .expectHeader().valueEquals("X-Custom-Header", "42"); }); } @Test void shouldExposeSchemaEndpoint() { - testWithWebClient((client) -> client.get().uri("/schema").accept(MediaType.ALL).exchange() + testWithWebClient((client) -> client.get().uri("/graphql/schema").accept(MediaType.ALL).exchange() .expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_PLAIN).expectBody(String.class) .value(containsString("type Book"))); } + @Test + void shouldExposeGraphiqlEndpoint() { + testWithWebClient((client) -> { + client.get().uri("/graphiql").exchange().expectStatus().is3xxRedirection().expectHeader() + .location("https://spring.example.org/graphiql?path=/graphql"); + client.get().uri("/graphiql?path=/graphql").accept(MediaType.ALL).exchange().expectStatus().isOk() + .expectHeader().contentType(MediaType.TEXT_HTML); + }); + } + private void testWithWebClient(Consumer consumer) { this.contextRunner.run((context) -> { WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient() diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java index 37c1f16f29..1b9212d16b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java @@ -43,6 +43,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -57,8 +58,9 @@ class GraphQlWebMvcAutoConfigurationTests { AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class)) - .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class).withPropertyValues( - "spring.main.web-application-type=servlet", "spring.graphql.schema.printer.enabled=true"); + .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) + .withPropertyValues("spring.main.web-application-type=servlet", "spring.graphql.graphiql.enabled=true", + "spring.graphql.schema.printer.enabled=true"); @Test void simpleQueryShouldWork() { @@ -107,6 +109,16 @@ class GraphQlWebMvcAutoConfigurationTests { .andExpect(content().string(Matchers.containsString("type Book")))); } + @Test + void shouldExposeGraphiqlEndpoint() { + testWith((mockMvc) -> { + mockMvc.perform(get("/graphiql")).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/graphiql?path=/graphql")); + mockMvc.perform(get("/graphiql?path=/graphql")).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.TEXT_HTML)); + }); + } + private void testWith(MockMvcConsumer mockMvcConsumer) { this.contextRunner.run((context) -> { MediaType mediaType = MediaType.APPLICATION_JSON;