Add schema printer endpoint

This commit auto-configures a web endpoint that prints the GraphQL
schema of the application.

Closes gh-13
This commit is contained in:
Brian Clozel
2021-05-11 11:54:17 +02:00
parent ee51f26755
commit 2d97e326e2
11 changed files with 137 additions and 40 deletions

View File

@@ -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
````

View File

@@ -57,9 +57,7 @@ public class GraphQLAutoConfiguration {
public GraphQLSource.Builder graphQLSourceBuilder(
GraphQLProperties properties, RuntimeWiring runtimeWiring,
ResourceLoader resourceLoader, ObjectProvider<Instrumentation> instrumentationsProvider) {
String schemaLocation = properties.getSchemaLocation();
String schemaLocation = properties.getSchema().getLocation();
return GraphQLSource.builder()
.schemaResource(resourceLoader.getResource(schemaLocation))
.runtimeWiring(runtimeWiring)

View File

@@ -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.

View File

@@ -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<ServerResponse> graphQLEndpoint(
GraphQLHttpHandler handler, GraphQLProperties properties, ResourceLoader resourceLoader) {
public RouterFunction<ServerResponse> 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")

View File

@@ -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<ServerResponse> graphQLQueryEndpoint(
ResourceLoader resourceLoader, GraphQLHttpHandler handler, GraphQLProperties properties) {
public RouterFunction<ServerResponse> 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();
}

View File

@@ -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");

View File

@@ -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<WebTestClient> 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);
}

View File

@@ -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)

View File

@@ -1 +1,3 @@
management.endpoints.web.exposure.include=health,metrics,info
management.endpoints.web.exposure.include=health,metrics,info
spring.graphql.schema.printer.enabled=true

View File

@@ -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;
}
}
}

View File

@@ -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