From 2d97e326e2d6cd66ad882994e5af53ce34eea830 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 11 May 2021 11:54:17 +0200 Subject: [PATCH] Add schema printer endpoint This commit auto-configures a web endpoint that prints the GraphQL schema of the application. Closes gh-13 --- README.md | 8 +- .../boot/GraphQLAutoConfiguration.java | 4 +- .../graphql/boot/GraphQLProperties.java | 73 +++++++++++++++---- .../boot/WebFluxGraphQLAutoConfiguration.java | 18 +++-- .../boot/WebMvcGraphQLAutoConfiguration.java | 18 +++-- .../boot/GraphQLAutoConfigurationTests.java | 4 +- .../boot/WebFluxApplicationContextTests.java | 14 +++- .../boot/WebMvcApplicationContextTests.java | 15 +++- .../src/main/resources/application.properties | 4 +- .../support/DefaultGraphQLSourceBuilder.java | 14 +++- .../graphql/support/GraphQLSource.java | 5 ++ 11 files changed, 137 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 7a9624aa..04d32032 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,12 @@ The Spring GraphQL project offers a few configuration properties to customize yo # web path to the graphql endpoint spring.graphql.path=/graphql # location of the graphql schema file -spring.graphql.schema-location=classpath:/schema.graphqls -# Whether micrometer metrics should be collected for graphql queries +spring.graphql.schema.location=classpath:/schema.graphqls +# schema printer endpoint configuration +# endpoint path is concatenated with the main path, so "/graphql/schema" by default +spring.graphql.schema.printer.enabled=false +spring.graphql.schema.printer.path=/schema +# whether micrometer metrics should be collected for graphql queries management.metrics.graphql.autotime.enabled=true ```` diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLAutoConfiguration.java index d47f4a0c..f918def9 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLAutoConfiguration.java @@ -57,9 +57,7 @@ public class GraphQLAutoConfiguration { public GraphQLSource.Builder graphQLSourceBuilder( GraphQLProperties properties, RuntimeWiring runtimeWiring, ResourceLoader resourceLoader, ObjectProvider instrumentationsProvider) { - - String schemaLocation = properties.getSchemaLocation(); - + String schemaLocation = properties.getSchema().getLocation(); return GraphQLSource.builder() .schemaResource(resourceLoader.getResource(schemaLocation)) .runtimeWiring(runtimeWiring) diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLProperties.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLProperties.java index bc992aa2..ec07d7a3 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLProperties.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQLProperties.java @@ -22,17 +22,14 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.graphql") public class GraphQLProperties { - /** - * Location of the GraphQL schema file. - */ - private String schemaLocation = "classpath:schema.graphqls"; - /** * Path of the GraphQL HTTP query endpoint. */ private String path = "/graphql"; - private WebSocket websocket = new WebSocket(); + private final Schema schema = new Schema(); + + private final WebSocket websocket = new WebSocket(); public String getPath() { return this.path; @@ -42,19 +39,69 @@ public class GraphQLProperties { this.path = path; } - public String getSchemaLocation() { - return this.schemaLocation; - } - - public void setSchemaLocation(String schemaLocation) { - this.schemaLocation = schemaLocation; + public Schema getSchema() { + return this.schema; } public WebSocket getWebsocket() { return this.websocket; } - static class WebSocket { + public static class Schema { + + /** + * Location of the GraphQL schema file. + */ + private String location = "classpath:schema.graphqls"; + + private final Printer printer = new Printer(); + + public String getLocation() { + return this.location; + } + + public void setLocation(String location) { + this.location = location; + } + + public Printer getPrinter() { + return this.printer; + } + + + public static class Printer { + + /** + * Whether the endpoint that prints the schema is enabled. + */ + private boolean enabled = false; + + /** + * Path under the main GraphQL path where the schema is exposed. + */ + private String path = "/schema"; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + } + + } + + + public static class WebSocket { /** * Path of the GraphQL WebSocket subscription endpoint. diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java index 08f33402..baee72cc 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebFluxGraphQLAutoConfiguration.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.stream.Collectors; import graphql.GraphQL; +import graphql.schema.idl.SchemaPrinter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; @@ -75,20 +76,23 @@ public class WebFluxGraphQLAutoConfiguration { } @Bean - public RouterFunction graphQLEndpoint( - GraphQLHttpHandler handler, GraphQLProperties properties, ResourceLoader resourceLoader) { + public RouterFunction graphQLEndpoint(GraphQLHttpHandler handler, GraphQLSource graphQLSource, + GraphQLProperties properties, ResourceLoader resourceLoader) { String path = properties.getPath(); Resource resource = resourceLoader.getResource("classpath:graphiql/index.html"); - if (logger.isInfoEnabled()) { logger.info("GraphQL endpoint HTTP POST " + path); } - - return RouterFunctions.route() + RouterFunctions.Builder builder = RouterFunctions.route() .GET(path, req -> ServerResponse.ok().bodyValue(resource)) - .POST(path, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), handler::handleQuery) - .build(); + .POST(path, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), handler::handleQuery); + if (properties.getSchema().getPrinter().isEnabled()) { + SchemaPrinter schemaPrinter = new SchemaPrinter(); + builder = builder.GET(path + properties.getSchema().getPrinter().getPath(), + req -> ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).bodyValue(schemaPrinter.print(graphQLSource.schema()))); + } + return builder.build(); } @ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path") diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java index 1c588869..77616a37 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/WebMvcGraphQLAutoConfiguration.java @@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest; import javax.websocket.server.ServerContainer; import graphql.GraphQL; +import graphql.schema.idl.SchemaPrinter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -82,20 +83,23 @@ public class WebMvcGraphQLAutoConfiguration { } @Bean - public RouterFunction graphQLQueryEndpoint( - ResourceLoader resourceLoader, GraphQLHttpHandler handler, GraphQLProperties properties) { + public RouterFunction graphQLQueryEndpoint(GraphQLHttpHandler handler, GraphQLSource graphQLSource, + GraphQLProperties properties, ResourceLoader resourceLoader) { String path = properties.getPath(); Resource resource = resourceLoader.getResource("classpath:graphiql/index.html"); - if (logger.isInfoEnabled()) { logger.info("GraphQL endpoint HTTP POST " + path); } - - return RouterFunctions.route() + RouterFunctions.Builder builder = RouterFunctions.route() .GET(path, req -> ServerResponse.ok().body(resource)) - .POST(path, contentType(MediaType.APPLICATION_JSON).and(accept(MediaType.APPLICATION_JSON)), handler::handle) - .build(); + .POST(path, contentType(MediaType.APPLICATION_JSON).and(accept(MediaType.APPLICATION_JSON)), handler::handle); + if (properties.getSchema().getPrinter().isEnabled()) { + SchemaPrinter schemaPrinter = new SchemaPrinter(); + builder = builder.GET(path + properties.getSchema().getPrinter().getPath(), + req -> ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(schemaPrinter.print(graphQLSource.schema()))); + } + return builder.build(); } diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQLAutoConfigurationTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQLAutoConfigurationTests.java index 483c854a..7e261387 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQLAutoConfigurationTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQLAutoConfigurationTests.java @@ -47,14 +47,14 @@ class GraphQLAutoConfigurationTests { @Test void shouldCreateBuilderWithSdl() { contextRunner - .withPropertyValues("spring.graphql.schema-location:classpath:books/schema.graphqls") + .withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls") .run((context) -> assertThat(context).hasSingleBean(GraphQLSource.class)); } @Test void shouldUseProgrammaticallyDefinedBuilder() { contextRunner - .withPropertyValues("spring.graphql.schema-location:classpath:books/schema.graphqls") + .withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls") .withUserConfiguration(CustomGraphQLBuilderConfiguration.class) .run((context) -> { assertThat(context).hasBean("customGraphQLSourceBuilder"); diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java index 75e084cf..b10d99ab 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java @@ -18,6 +18,7 @@ package org.springframework.graphql.boot; import java.util.Collections; import java.util.function.Consumer; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -97,6 +98,16 @@ class WebFluxApplicationContextTests { }); } + @Test + void schemaEndpoint() { + testWithWebClient(client -> { + client.get().uri("/schema").accept(MediaType.ALL).exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.TEXT_PLAIN) + .expectBody(String.class).value(Matchers.containsString("type Book")); + }); + } + private void testWithWebClient(Consumer consumer) { testWithApplicationContext(context -> { WebTestClient client = WebTestClient.bindToApplicationContext(context) @@ -117,7 +128,8 @@ class WebFluxApplicationContextTests { .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) .withPropertyValues( "spring.main.web-application-type=reactive", - "spring.graphql.schema-location:classpath:books/schema.graphqls") + "spring.graphql.schema.printer.enabled=true", + "spring.graphql.schema.location=classpath:books/schema.graphqls") .run(consumer); } diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java index 1d13736d..beab8dc6 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java @@ -17,6 +17,7 @@ package org.springframework.graphql.boot; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -32,10 +33,12 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +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.header; @@ -97,13 +100,23 @@ class WebMvcApplicationContextTests { }); } + @Test + void schemaEndpoint() { + testWith(mockMvc -> { + mockMvc.perform(get("/graphql/schema")).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.TEXT_PLAIN)) + .andExpect(content().string(Matchers.containsString("type Book"))); + }); + } + private void testWith(MockMvcConsumer mockMvcConsumer) { new WebApplicationContextRunner() .withConfiguration(AUTO_CONFIGURATIONS) .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) .withPropertyValues( "spring.main.web-application-type=servlet", - "spring.graphql.schema-location:classpath:books/schema.graphqls") + "spring.graphql.schema.printer.enabled=true", + "spring.graphql.schema.location=classpath:books/schema.graphqls") .run((context) -> { MockHttpServletRequestBuilder builder = post("/graphql") .contentType(MediaType.APPLICATION_JSON) diff --git a/samples/webmvc-http/src/main/resources/application.properties b/samples/webmvc-http/src/main/resources/application.properties index d589e7c7..28e043a8 100644 --- a/samples/webmvc-http/src/main/resources/application.properties +++ b/samples/webmvc-http/src/main/resources/application.properties @@ -1 +1,3 @@ -management.endpoints.web.exposure.include=health,metrics,info \ No newline at end of file +management.endpoints.web.exposure.include=health,metrics,info + +spring.graphql.schema.printer.enabled=true \ No newline at end of file diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQLSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQLSourceBuilder.java index f468f583..711f5cb0 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQLSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/DefaultGraphQLSourceBuilder.java @@ -107,7 +107,7 @@ class DefaultGraphQLSourceBuilder implements GraphQLSource.Builder { this.graphQLConfigurers.accept(builder); GraphQL graphQL = builder.build(); - return new CachedGraphQLSource(graphQL); + return new CachedGraphQLSource(graphQL, schema); } private TypeDefinitionRegistry parseSchemaResource() { @@ -126,20 +126,28 @@ class DefaultGraphQLSourceBuilder implements GraphQLSource.Builder { /** - * GraphQLSource that returns the built GraphQL instance. + * GraphQLSource that returns the built GraphQL instance and its schema. */ private static class CachedGraphQLSource implements GraphQLSource { private final GraphQL graphQL; - CachedGraphQLSource(GraphQL graphQL) { + private final GraphQLSchema schema; + + CachedGraphQLSource(GraphQL graphQL, GraphQLSchema schema) { this.graphQL = graphQL; + this.schema = schema; } @Override public GraphQL graphQL() { return this.graphQL; } + + @Override + public GraphQLSchema schema() { + return this.schema; + } } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/support/GraphQLSource.java b/spring-graphql/src/main/java/org/springframework/graphql/support/GraphQLSource.java index 706be72d..568da443 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/support/GraphQLSource.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/support/GraphQLSource.java @@ -45,6 +45,11 @@ public interface GraphQLSource { */ GraphQL graphQL(); + /** + * Return the {@link GraphQLSchema} used by the current {@link GraphQL}. + */ + GraphQLSchema schema(); + /** * Return a builder for a {@link GraphQLSource} given input for the