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:
@@ -119,8 +119,8 @@ The Spring GraphQL project offers a few configuration properties to customize yo
|
||||
````properties
|
||||
# web path to the graphql endpoint
|
||||
spring.graphql.path=/graphql
|
||||
# location of the graphql schema file
|
||||
spring.graphql.schema.location=classpath:graphql/schema.graphqls
|
||||
# locations of the graphql '*.graphqls' schema files
|
||||
spring.graphql.schema.locations=classpath:graphql/
|
||||
# schema printer endpoint configuration
|
||||
# endpoint path is concatenated with the main path, so "/graphql/schema" by default
|
||||
spring.graphql.schema.printer.enabled=false
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.graphql.execution;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -34,7 +35,6 @@ import graphql.schema.idl.SchemaParser;
|
||||
import graphql.schema.idl.TypeDefinitionRegistry;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -42,11 +42,11 @@ import org.springframework.util.Assert;
|
||||
* {@link GraphQL} instance and wraps it with a {@link GraphQlSource} that returns it.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
|
||||
@Nullable
|
||||
private Resource schemaResource;
|
||||
private List<Resource> schemaResources = new ArrayList<>();
|
||||
|
||||
private RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build();
|
||||
|
||||
@@ -64,8 +64,8 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlSource.Builder schemaResource(Resource resource) {
|
||||
this.schemaResource = resource;
|
||||
public GraphQlSource.Builder schemaResources(Resource... resources) {
|
||||
this.schemaResources.addAll(Arrays.asList(resources));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,9 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
|
||||
@Override
|
||||
public GraphQlSource build() {
|
||||
TypeDefinitionRegistry registry = parseSchemaResource();
|
||||
TypeDefinitionRegistry registry = this.schemaResources.stream()
|
||||
.map(this::parseSchemaResource).reduce(TypeDefinitionRegistry::merge)
|
||||
.orElseThrow(() -> new IllegalArgumentException("'schemaResources' should not be empty"));
|
||||
|
||||
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(registry, this.runtimeWiring);
|
||||
for (GraphQLTypeVisitor visitor : this.typeVisitors) {
|
||||
@@ -120,16 +122,16 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
return new CachedGraphQlSource(graphQl, schema);
|
||||
}
|
||||
|
||||
private TypeDefinitionRegistry parseSchemaResource() {
|
||||
Assert.notNull(this.schemaResource, "'schemaResource' not provided");
|
||||
Assert.isTrue(this.schemaResource.exists(), "'schemaResource' does not exist");
|
||||
private TypeDefinitionRegistry parseSchemaResource(Resource schemaResource) {
|
||||
Assert.notNull(schemaResource, "'schemaResource' not provided");
|
||||
Assert.isTrue(schemaResource.exists(), "'schemaResource' does not exist");
|
||||
try {
|
||||
try (InputStream inputStream = this.schemaResource.getInputStream()) {
|
||||
try (InputStream inputStream = schemaResource.getInputStream()) {
|
||||
return new SchemaParser().parse(inputStream);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalArgumentException("Failed to load resourceLocation " + this.schemaResource.toString());
|
||||
throw new IllegalArgumentException("Failed to load schema resource: " + schemaResource.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,12 +70,13 @@ public interface GraphQlSource {
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
* Provide the resource for the GraphQL {@literal ".schema"} file to parse.
|
||||
* @param resource the resource for the GraphQL schema
|
||||
* Add {@literal ".graphqls"} schema resources to be
|
||||
* {@link TypeDefinitionRegistry#merge(TypeDefinitionRegistry) merged} into the type registry.
|
||||
* @param resources resources for the GraphQL schema
|
||||
* @return the current builder
|
||||
* @see graphql.schema.idl.SchemaParser#parse(File)
|
||||
*/
|
||||
Builder schemaResource(Resource resource);
|
||||
Builder schemaResources(Resource... resources);
|
||||
|
||||
/**
|
||||
* Set a {@link RuntimeWiring} to contribute data fetchers and more.
|
||||
|
||||
@@ -57,7 +57,7 @@ public abstract class GraphQlTestUtils {
|
||||
.build();
|
||||
|
||||
return GraphQlSource.builder()
|
||||
.schemaResource(new ByteArrayResource(schemaContent.getBytes(StandardCharsets.UTF_8)))
|
||||
.schemaResources(new ByteArrayResource(schemaContent.getBytes(StandardCharsets.UTF_8)))
|
||||
.runtimeWiring(wiring);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ class QuerydslDataFetcherTests {
|
||||
configurer.accept(wiringBuilder);
|
||||
builder.type(wiringBuilder);
|
||||
return GraphQlSource.builder()
|
||||
.schemaResource(new ClassPathResource("books/schema.graphqls"))
|
||||
.schemaResources(new ClassPathResource("books/schema.graphqls"))
|
||||
.runtimeWiring(builder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public abstract class BookTestUtils {
|
||||
return Flux.fromIterable(booksMap.values()).filter((book) -> book.getAuthor().contains(author));
|
||||
}));
|
||||
return GraphQlSource.builder()
|
||||
.schemaResource(new ClassPathResource("books/schema.graphqls"))
|
||||
.schemaResources(new ClassPathResource("books/schema.graphqls"))
|
||||
.runtimeWiring(builder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user