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
This commit is contained in:
parent
ff9a421786
commit
0099460155
|
|
@ -34,8 +34,14 @@ public class GraphQlProperties {
|
||||||
*/
|
*/
|
||||||
private String path = "/graphql";
|
private String path = "/graphql";
|
||||||
|
|
||||||
|
private final Graphiql graphiql = new Graphiql();
|
||||||
|
|
||||||
private final Schema schema = new Schema();
|
private final Schema schema = new Schema();
|
||||||
|
|
||||||
|
public Graphiql getGraphiql() {
|
||||||
|
return this.graphiql;
|
||||||
|
}
|
||||||
|
|
||||||
public String getPath() {
|
public String getPath() {
|
||||||
return this.path;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.GraphiQlHandler;
|
||||||
import org.springframework.graphql.web.webflux.SchemaHandler;
|
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;
|
||||||
|
|
@ -97,6 +98,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.getGraphiql().isEnabled()) {
|
||||||
|
GraphiQlHandler graphiQlHandler = new GraphiQlHandler(graphQLPath);
|
||||||
|
builder = builder.GET(properties.getGraphiql().getPath(), graphiQlHandler::handleRequest);
|
||||||
|
}
|
||||||
|
|
||||||
if (properties.getSchema().getPrinter().isEnabled()) {
|
if (properties.getSchema().getPrinter().isEnabled()) {
|
||||||
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
||||||
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
|
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
|
||||||
|
|
|
||||||
|
|
@ -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.GraphiQlHandler;
|
||||||
import org.springframework.graphql.web.webmvc.SchemaHandler;
|
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;
|
||||||
|
|
@ -98,6 +99,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.getGraphiql().isEnabled()) {
|
||||||
|
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(graphQLPath);
|
||||||
|
builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);
|
||||||
|
}
|
||||||
|
|
||||||
if (properties.getSchema().getPrinter().isEnabled()) {
|
if (properties.getSchema().getPrinter().isEnabled()) {
|
||||||
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
||||||
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
|
builder = builder.GET(graphQLPath + "/schema", schemaHandler::handleRequest);
|
||||||
|
|
|
||||||
|
|
@ -47,20 +47,21 @@ import static org.hamcrest.Matchers.containsString;
|
||||||
*/
|
*/
|
||||||
class GraphQlWebFluxAutoConfigurationTests {
|
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()
|
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
|
||||||
.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).withPropertyValues(
|
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
|
||||||
"spring.main.web-application-type=reactive", "spring.graphql.schema.printer.enabled=true");
|
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.graphiql.enabled=true",
|
||||||
|
"spring.graphql.schema.printer.enabled=true");
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void simpleQueryShouldWork() {
|
void simpleQueryShouldWork() {
|
||||||
testWithWebClient((client) -> {
|
testWithWebClient((client) -> {
|
||||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
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");
|
.expectBody().jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -69,19 +70,21 @@ class GraphQlWebFluxAutoConfigurationTests {
|
||||||
void httpGetQueryShouldBeSupported() {
|
void httpGetQueryShouldBeSupported() {
|
||||||
testWithWebClient((client) -> {
|
testWithWebClient((client) -> {
|
||||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
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");
|
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED).expectHeader().valueEquals("Allow", "POST");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectMissingQuery() {
|
void shouldRejectMissingQuery() {
|
||||||
testWithWebClient((client) -> client.post().uri("").bodyValue("{}").exchange().expectStatus().isBadRequest());
|
testWithWebClient(
|
||||||
|
(client) -> client.post().uri("/graphql").bodyValue("{}").exchange().expectStatus().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectQueryWithInvalidJson() {
|
void shouldRejectQueryWithInvalidJson() {
|
||||||
testWithWebClient((client) -> client.post().uri("").bodyValue(":)").exchange().expectStatus().isBadRequest());
|
testWithWebClient(
|
||||||
|
(client) -> client.post().uri("/graphql").bodyValue(":)").exchange().expectStatus().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -89,18 +92,28 @@ class GraphQlWebFluxAutoConfigurationTests {
|
||||||
testWithWebClient((client) -> {
|
testWithWebClient((client) -> {
|
||||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
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");
|
.expectHeader().valueEquals("X-Custom-Header", "42");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExposeSchemaEndpoint() {
|
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)
|
.expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_PLAIN).expectBody(String.class)
|
||||||
.value(containsString("type Book")));
|
.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<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()
|
||||||
|
|
|
||||||
|
|
@ -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.content;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
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.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,8 +58,9 @@ 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).withPropertyValues(
|
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
|
||||||
"spring.main.web-application-type=servlet", "spring.graphql.schema.printer.enabled=true");
|
.withPropertyValues("spring.main.web-application-type=servlet", "spring.graphql.graphiql.enabled=true",
|
||||||
|
"spring.graphql.schema.printer.enabled=true");
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void simpleQueryShouldWork() {
|
void simpleQueryShouldWork() {
|
||||||
|
|
@ -107,6 +109,16 @@ class GraphQlWebMvcAutoConfigurationTests {
|
||||||
.andExpect(content().string(Matchers.containsString("type Book"))));
|
.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) {
|
private void testWith(MockMvcConsumer mockMvcConsumer) {
|
||||||
this.contextRunner.run((context) -> {
|
this.contextRunner.run((context) -> {
|
||||||
MediaType mediaType = MediaType.APPLICATION_JSON;
|
MediaType mediaType = MediaType.APPLICATION_JSON;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue