Auto-configure schema printer endpoint

This commit configuresa new endpoint for printing in text format the
resolved GraphQL schema.
This endpoint is exposed by default under "/graphql/schema" and must be
enabled with "spring.graphql.schema.printer=true".

See gh-29140
This commit is contained in:
Brian Clozel 2021-12-21 08:33:05 +01:00
parent b38d04556e
commit ff9a421786
5 changed files with 57 additions and 4 deletions

View File

@ -60,6 +60,8 @@ public class GraphQlProperties {
*/ */
private String[] fileExtensions = new String[] { ".graphqls", ".gqls" }; private String[] fileExtensions = new String[] { ".graphqls", ".gqls" };
private final Printer printer = new Printer();
public String[] getLocations() { public String[] getLocations() {
return this.locations; return this.locations;
} }
@ -81,6 +83,28 @@ public class GraphQlProperties {
.toArray(String[]::new); .toArray(String[]::new);
} }
public Printer getPrinter() {
return this.printer;
}
public static class Printer {
/**
* Whether the endpoint that prints the schema is enabled. Schema is available
* under spring.graphql.path + "/schema".
*/
private boolean enabled = false;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
} }
} }

View File

@ -40,6 +40,7 @@ import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInterceptor; import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.webflux.GraphQlHttpHandler; import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
import org.springframework.graphql.web.webflux.SchemaHandler;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -96,6 +97,11 @@ public class GraphQlWebFluxAutoConfiguration {
.POST(graphQLPath, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)), .POST(graphQLPath, accept(MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_JSON)),
handler::handleRequest); handler::handleRequest);
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
}
return builder.build(); return builder.build();
} }

View File

@ -41,6 +41,7 @@ import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInterceptor; import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler; import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
import org.springframework.graphql.web.webmvc.SchemaHandler;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -97,6 +98,11 @@ public class GraphQlWebMvcAutoConfiguration {
.POST(graphQLPath, RequestPredicates.contentType(MediaType.APPLICATION_JSON) .POST(graphQLPath, RequestPredicates.contentType(MediaType.APPLICATION_JSON)
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest); .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::handleRequest);
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
}
return builder.build(); return builder.build();
} }

View File

@ -38,6 +38,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import static org.hamcrest.Matchers.containsString;
/** /**
* Tests for {@link GraphQlWebFluxAutoConfiguration} * Tests for {@link GraphQlWebFluxAutoConfiguration}
* *
@ -51,8 +53,8 @@ class GraphQlWebFluxAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, .withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class, CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlWebFluxAutoConfiguration.class)) GraphQlWebFluxAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class).withPropertyValues(
.withPropertyValues("spring.main.web-application-type=reactive"); "spring.main.web-application-type=reactive", "spring.graphql.schema.printer.enabled=true");
@Test @Test
void simpleQueryShouldWork() { void simpleQueryShouldWork() {
@ -92,6 +94,13 @@ class GraphQlWebFluxAutoConfigurationTests {
}); });
} }
@Test
void shouldExposeSchemaEndpoint() {
testWithWebClient((client) -> client.get().uri("/schema").accept(MediaType.ALL).exchange()
.expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_PLAIN).expectBody(String.class)
.value(containsString("type Book")));
}
private void testWithWebClient(Consumer<WebTestClient> consumer) { private void testWithWebClient(Consumer<WebTestClient> consumer) {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient() WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient()

View File

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.graphql.servlet; package org.springframework.boot.autoconfigure.graphql.servlet;
import graphql.schema.idl.TypeRuntimeWiring; import graphql.schema.idl.TypeRuntimeWiring;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
@ -56,8 +57,8 @@ class GraphQlWebMvcAutoConfigurationTests {
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class)) GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class) .withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class).withPropertyValues(
.withPropertyValues("spring.main.web-application-type=servlet"); "spring.main.web-application-type=servlet", "spring.graphql.schema.printer.enabled=true");
@Test @Test
void simpleQueryShouldWork() { void simpleQueryShouldWork() {
@ -99,6 +100,13 @@ class GraphQlWebMvcAutoConfigurationTests {
}); });
} }
@Test
void shouldExposeSchemaEndpoint() {
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) { private void testWith(MockMvcConsumer mockMvcConsumer) {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
MediaType mediaType = MediaType.APPLICATION_JSON; MediaType mediaType = MediaType.APPLICATION_JSON;