Load multiple GraphQL schema files

Prior to this commit, the `spring.graphql.schema.location` would only
allow a single file as the source for the GraphQL schema.

This commit changes this configuration to
`spring.graphql.schema.locations` so that it accepts actual locations
(folders) and scans for `*.graphqls` files in those locations. All files
are parsed and merged as a single GraphQL `TypeDefinitionRegistry`.

Closes gh-56
This commit is contained in:
Brian Clozel
2021-06-29 21:27:18 +02:00
parent f29c5ba764
commit fdf446f3e6
11 changed files with 66 additions and 37 deletions

View File

@@ -16,6 +16,10 @@
package org.springframework.graphql.boot;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
@@ -27,9 +31,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.GraphQlSource;
@@ -55,6 +61,8 @@ public class GraphQlAutoConfiguration {
@ConditionalOnMissingBean(GraphQlSource.Builder.class)
public static class GraphQlSourceConfiguration {
private static final String SCHEMA_FILES_PATTERN = "*.graphqls";
@Bean
@ConditionalOnMissingBean
public RuntimeWiring runtimeWiring(ObjectProvider<RuntimeWiringCustomizer> customizers) {
@@ -64,17 +72,25 @@ public class GraphQlAutoConfiguration {
}
@Bean
public GraphQlSource.Builder graphQlSourceBuilder(GraphQlProperties properties, RuntimeWiring runtimeWiring,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider, ResourceLoader resourceLoader,
ObjectProvider<Instrumentation> instrumentationsProvider) {
public GraphQlSource.Builder graphQlSourceBuilder(ApplicationContext applicationContext, GraphQlProperties properties,
RuntimeWiring runtimeWiring, ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
ObjectProvider<Instrumentation> instrumentationsProvider) throws IOException {
String schemaLocation = properties.getSchema().getLocation();
return GraphQlSource.builder().schemaResource(resourceLoader.getResource(schemaLocation))
List<Resource> schemaResources = resolveSchemaResources(applicationContext, properties.getSchema().getLocations());
return GraphQlSource.builder().schemaResources(schemaResources.toArray(new Resource[0]))
.runtimeWiring(runtimeWiring)
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, List<String> schemaLocations) throws IOException {
List<Resource> schemaResources = new ArrayList<>();
for (String location : schemaLocations) {
schemaResources.addAll(Arrays.asList(resolver.getResources(location + SCHEMA_FILES_PATTERN)));
}
return schemaResources;
}
}
}

View File

@@ -17,6 +17,10 @@
package org.springframework.graphql.boot;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -63,18 +67,24 @@ public class GraphQlProperties {
public static class Schema {
/**
* Location of the GraphQL schema file.
* Locations of GraphQL '*.graphqls' schema files.
*/
private String location = "classpath:graphql/schema.graphqls";
private List<String> locations = new ArrayList<>(Collections.singletonList("classpath:graphql/"));
private final Printer printer = new Printer();
public String getLocation() {
return this.location;
public List<String> getLocations() {
return this.locations;
}
public void setLocation(String location) {
this.location = location;
public void setLocations(List<String> locations) {
this.locations = appendSlashIfNecessary(locations);
}
private List<String> appendSlashIfNecessary(List<String> locations) {
return locations.stream()
.map(location -> location.endsWith("/") ? location : location + "/")
.collect(Collectors.toList());
}
public Printer getPrinter() {

View File

@@ -39,19 +39,19 @@ class GraphQlAutoConfigurationTests {
void shouldFailWhenSchemaFileIsMissing() {
this.contextRunner.run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().getRootCause().hasMessage("'schemaResource' does not exist");
assertThat(context).getFailure().getRootCause().hasMessage("'schemaResources' should not be empty");
});
}
@Test
void shouldCreateBuilderWithSdl() {
this.contextRunner.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:books/")
.run((context) -> assertThat(context).hasSingleBean(GraphQlSource.class));
}
@Test
void shouldUseProgrammaticallyDefinedBuilder() {
this.contextRunner.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:books/")
.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
assertThat(context).hasBean("customGraphQlSourceBuilder");
assertThat(context).hasSingleBean(GraphQlSource.Builder.class);
@@ -63,7 +63,7 @@ class GraphQlAutoConfigurationTests {
@Bean
GraphQlSource.Builder customGraphQlSourceBuilder() {
return GraphQlSource.builder().schemaResource(new ClassPathResource("books/schema.graphqls"));
return GraphQlSource.builder().schemaResources(new ClassPathResource("books/schema.graphqls"));
}
}

View File

@@ -144,7 +144,7 @@ class WebFluxApplicationContextTests {
.withPropertyValues(
"spring.main.web-application-type=reactive",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.schema.location=classpath:books/schema.graphqls")
"spring.graphql.schema.locations=classpath:books/")
.run(consumer);
}

View File

@@ -114,7 +114,7 @@ class WebMvcApplicationContextTests {
.withPropertyValues(
"spring.main.web-application-type=servlet",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.schema.location=classpath:books/schema.graphqls")
"spring.graphql.schema.locations=classpath:books/")
.run((context) -> {
MediaType mediaType = MediaType.APPLICATION_JSON;
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context)