From fdf446f3e662565c98d77c19bd609bbdb4f92a5b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 29 Jun 2021 21:27:18 +0200 Subject: [PATCH] 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 --- README.md | 4 +-- .../boot/GraphQlAutoConfiguration.java | 28 +++++++++++++++---- .../graphql/boot/GraphQlProperties.java | 22 +++++++++++---- .../boot/GraphQlAutoConfigurationTests.java | 8 +++--- .../boot/WebFluxApplicationContextTests.java | 2 +- .../boot/WebMvcApplicationContextTests.java | 2 +- .../DefaultGraphQlSourceBuilder.java | 24 ++++++++-------- .../graphql/execution/GraphQlSource.java | 7 +++-- .../graphql/GraphQlTestUtils.java | 2 +- .../data/QuerydslDataFetcherTests.java | 2 +- .../graphql/web/BookTestUtils.java | 2 +- 11 files changed, 66 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 1544e308..8345fc68 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java index 1fb95faa..9f04378b 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java @@ -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 customizers) { @@ -64,17 +72,25 @@ public class GraphQlAutoConfiguration { } @Bean - public GraphQlSource.Builder graphQlSourceBuilder(GraphQlProperties properties, RuntimeWiring runtimeWiring, - ObjectProvider exceptionResolversProvider, ResourceLoader resourceLoader, - ObjectProvider instrumentationsProvider) { + public GraphQlSource.Builder graphQlSourceBuilder(ApplicationContext applicationContext, GraphQlProperties properties, + RuntimeWiring runtimeWiring, ObjectProvider exceptionResolversProvider, + ObjectProvider instrumentationsProvider) throws IOException { - String schemaLocation = properties.getSchema().getLocation(); - return GraphQlSource.builder().schemaResource(resourceLoader.getResource(schemaLocation)) + List 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 resolveSchemaResources(ResourcePatternResolver resolver, List schemaLocations) throws IOException { + List schemaResources = new ArrayList<>(); + for (String location : schemaLocations) { + schemaResources.addAll(Arrays.asList(resolver.getResources(location + SCHEMA_FILES_PATTERN))); + } + return schemaResources; + } + } } diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlProperties.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlProperties.java index bb3b2afd..b57be4ce 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlProperties.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlProperties.java @@ -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 locations = new ArrayList<>(Collections.singletonList("classpath:graphql/")); private final Printer printer = new Printer(); - public String getLocation() { - return this.location; + public List getLocations() { + return this.locations; } - public void setLocation(String location) { - this.location = location; + public void setLocations(List locations) { + this.locations = appendSlashIfNecessary(locations); + } + + private List appendSlashIfNecessary(List locations) { + return locations.stream() + .map(location -> location.endsWith("/") ? location : location + "/") + .collect(Collectors.toList()); } public Printer getPrinter() { diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java index 174e4de4..55fca42b 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java @@ -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")); } } diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java index 4d2aa0e6..81a5f11d 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java @@ -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); } diff --git a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java index b01f7860..a87eb4f9 100644 --- a/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java +++ b/graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java @@ -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) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java index d1478072..9bfec344 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java @@ -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 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()); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java index 16491478..1f40e2a2 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java @@ -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. diff --git a/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java b/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java index 2c0bee12..da7054e0 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java @@ -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); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java index ce8b7fe4..89ce8629 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java @@ -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(); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java b/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java index 208491c1..db974624 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/web/BookTestUtils.java @@ -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(); }