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:
@@ -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
|
||||
````
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user