Skip to content

Commit fdf446f

Browse files
committed
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
1 parent f29c5ba commit fdf446f

File tree

11 files changed

+66
-37
lines changed

11 files changed

+66
-37
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ The Spring GraphQL project offers a few configuration properties to customize yo
119119
````properties
120120
# web path to the graphql endpoint
121121
spring.graphql.path=/graphql
122-
# location of the graphql schema file
123-
spring.graphql.schema.location=classpath:graphql/schema.graphqls
122+
# locations of the graphql '*.graphqls' schema files
123+
spring.graphql.schema.locations=classpath:graphql/
124124
# schema printer endpoint configuration
125125
# endpoint path is concatenated with the main path, so "/graphql/schema" by default
126126
spring.graphql.schema.printer.enabled=false

graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlAutoConfiguration.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.graphql.boot;
1818

19+
import java.io.IOException;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
1923
import java.util.stream.Collectors;
2024

2125
import graphql.GraphQL;
@@ -27,9 +31,11 @@
2731
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2832
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2933
import org.springframework.boot.context.properties.EnableConfigurationProperties;
34+
import org.springframework.context.ApplicationContext;
3035
import org.springframework.context.annotation.Bean;
3136
import org.springframework.context.annotation.Configuration;
32-
import org.springframework.core.io.ResourceLoader;
37+
import org.springframework.core.io.Resource;
38+
import org.springframework.core.io.support.ResourcePatternResolver;
3339
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
3440
import org.springframework.graphql.execution.GraphQlSource;
3541

@@ -55,6 +61,8 @@ public GraphQlSource graphQlSource(GraphQlSource.Builder builder) {
5561
@ConditionalOnMissingBean(GraphQlSource.Builder.class)
5662
public static class GraphQlSourceConfiguration {
5763

64+
private static final String SCHEMA_FILES_PATTERN = "*.graphqls";
65+
5866
@Bean
5967
@ConditionalOnMissingBean
6068
public RuntimeWiring runtimeWiring(ObjectProvider<RuntimeWiringCustomizer> customizers) {
@@ -64,17 +72,25 @@ public RuntimeWiring runtimeWiring(ObjectProvider<RuntimeWiringCustomizer> custo
6472
}
6573

6674
@Bean
67-
public GraphQlSource.Builder graphQlSourceBuilder(GraphQlProperties properties, RuntimeWiring runtimeWiring,
68-
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider, ResourceLoader resourceLoader,
69-
ObjectProvider<Instrumentation> instrumentationsProvider) {
75+
public GraphQlSource.Builder graphQlSourceBuilder(ApplicationContext applicationContext, GraphQlProperties properties,
76+
RuntimeWiring runtimeWiring, ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
77+
ObjectProvider<Instrumentation> instrumentationsProvider) throws IOException {
7078

71-
String schemaLocation = properties.getSchema().getLocation();
72-
return GraphQlSource.builder().schemaResource(resourceLoader.getResource(schemaLocation))
79+
List<Resource> schemaResources = resolveSchemaResources(applicationContext, properties.getSchema().getLocations());
80+
return GraphQlSource.builder().schemaResources(schemaResources.toArray(new Resource[0]))
7381
.runtimeWiring(runtimeWiring)
7482
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
7583
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
7684
}
7785

86+
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, List<String> schemaLocations) throws IOException {
87+
List<Resource> schemaResources = new ArrayList<>();
88+
for (String location : schemaLocations) {
89+
schemaResources.addAll(Arrays.asList(resolver.getResources(location + SCHEMA_FILES_PATTERN)));
90+
}
91+
return schemaResources;
92+
}
93+
7894
}
7995

8096
}

graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/GraphQlProperties.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
package org.springframework.graphql.boot;
1818

1919
import java.time.Duration;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
2024

2125
import org.springframework.boot.context.properties.ConfigurationProperties;
2226

@@ -63,18 +67,24 @@ public WebSocket getWebsocket() {
6367
public static class Schema {
6468

6569
/**
66-
* Location of the GraphQL schema file.
70+
* Locations of GraphQL '*.graphqls' schema files.
6771
*/
68-
private String location = "classpath:graphql/schema.graphqls";
72+
private List<String> locations = new ArrayList<>(Collections.singletonList("classpath:graphql/"));
6973

7074
private final Printer printer = new Printer();
7175

72-
public String getLocation() {
73-
return this.location;
76+
public List<String> getLocations() {
77+
return this.locations;
7478
}
7579

76-
public void setLocation(String location) {
77-
this.location = location;
80+
public void setLocations(List<String> locations) {
81+
this.locations = appendSlashIfNecessary(locations);
82+
}
83+
84+
private List<String> appendSlashIfNecessary(List<String> locations) {
85+
return locations.stream()
86+
.map(location -> location.endsWith("/") ? location : location + "/")
87+
.collect(Collectors.toList());
7888
}
7989

8090
public Printer getPrinter() {

graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/GraphQlAutoConfigurationTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,19 @@ class GraphQlAutoConfigurationTests {
3939
void shouldFailWhenSchemaFileIsMissing() {
4040
this.contextRunner.run((context) -> {
4141
assertThat(context).hasFailed();
42-
assertThat(context).getFailure().getRootCause().hasMessage("'schemaResource' does not exist");
42+
assertThat(context).getFailure().getRootCause().hasMessage("'schemaResources' should not be empty");
4343
});
4444
}
4545

4646
@Test
4747
void shouldCreateBuilderWithSdl() {
48-
this.contextRunner.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
48+
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:books/")
4949
.run((context) -> assertThat(context).hasSingleBean(GraphQlSource.class));
5050
}
5151

5252
@Test
5353
void shouldUseProgrammaticallyDefinedBuilder() {
54-
this.contextRunner.withPropertyValues("spring.graphql.schema.location:classpath:books/schema.graphqls")
54+
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:books/")
5555
.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
5656
assertThat(context).hasBean("customGraphQlSourceBuilder");
5757
assertThat(context).hasSingleBean(GraphQlSource.Builder.class);
@@ -63,7 +63,7 @@ static class CustomGraphQlBuilderConfiguration {
6363

6464
@Bean
6565
GraphQlSource.Builder customGraphQlSourceBuilder() {
66-
return GraphQlSource.builder().schemaResource(new ClassPathResource("books/schema.graphqls"));
66+
return GraphQlSource.builder().schemaResources(new ClassPathResource("books/schema.graphqls"));
6767
}
6868

6969
}

graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebFluxApplicationContextTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private void testWithApplicationContext(ContextConsumer<ApplicationContext> cons
144144
.withPropertyValues(
145145
"spring.main.web-application-type=reactive",
146146
"spring.graphql.schema.printer.enabled=true",
147-
"spring.graphql.schema.location=classpath:books/schema.graphqls")
147+
"spring.graphql.schema.locations=classpath:books/")
148148
.run(consumer);
149149
}
150150

graphql-spring-boot-starter/src/test/java/org/springframework/graphql/boot/WebMvcApplicationContextTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private void testWith(MockMvcConsumer mockMvcConsumer) {
114114
.withPropertyValues(
115115
"spring.main.web-application-type=servlet",
116116
"spring.graphql.schema.printer.enabled=true",
117-
"spring.graphql.schema.location=classpath:books/schema.graphqls")
117+
"spring.graphql.schema.locations=classpath:books/")
118118
.run((context) -> {
119119
MediaType mediaType = MediaType.APPLICATION_JSON;
120120
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context)

spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.io.InputStream;
2121
import java.util.ArrayList;
22+
import java.util.Arrays;
2223
import java.util.List;
2324
import java.util.function.Consumer;
2425

@@ -34,19 +35,18 @@
3435
import graphql.schema.idl.TypeDefinitionRegistry;
3536

3637
import org.springframework.core.io.Resource;
37-
import org.springframework.lang.Nullable;
3838
import org.springframework.util.Assert;
3939

4040
/**
4141
* Default implementation of {@link GraphQlSource.Builder} that initializes a
4242
* {@link GraphQL} instance and wraps it with a {@link GraphQlSource} that returns it.
4343
*
4444
* @author Rossen Stoyanchev
45+
* @author Brian Clozel
4546
*/
4647
class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
4748

48-
@Nullable
49-
private Resource schemaResource;
49+
private List<Resource> schemaResources = new ArrayList<>();
5050

5151
private RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build();
5252

@@ -64,8 +64,8 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
6464
}
6565

6666
@Override
67-
public GraphQlSource.Builder schemaResource(Resource resource) {
68-
this.schemaResource = resource;
67+
public GraphQlSource.Builder schemaResources(Resource... resources) {
68+
this.schemaResources.addAll(Arrays.asList(resources));
6969
return this;
7070
}
7171

@@ -102,7 +102,9 @@ public GraphQlSource.Builder configureGraphQl(Consumer<GraphQL.Builder> configur
102102

103103
@Override
104104
public GraphQlSource build() {
105-
TypeDefinitionRegistry registry = parseSchemaResource();
105+
TypeDefinitionRegistry registry = this.schemaResources.stream()
106+
.map(this::parseSchemaResource).reduce(TypeDefinitionRegistry::merge)
107+
.orElseThrow(() -> new IllegalArgumentException("'schemaResources' should not be empty"));
106108

107109
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(registry, this.runtimeWiring);
108110
for (GraphQLTypeVisitor visitor : this.typeVisitors) {
@@ -120,16 +122,16 @@ public GraphQlSource build() {
120122
return new CachedGraphQlSource(graphQl, schema);
121123
}
122124

123-
private TypeDefinitionRegistry parseSchemaResource() {
124-
Assert.notNull(this.schemaResource, "'schemaResource' not provided");
125-
Assert.isTrue(this.schemaResource.exists(), "'schemaResource' does not exist");
125+
private TypeDefinitionRegistry parseSchemaResource(Resource schemaResource) {
126+
Assert.notNull(schemaResource, "'schemaResource' not provided");
127+
Assert.isTrue(schemaResource.exists(), "'schemaResource' does not exist");
126128
try {
127-
try (InputStream inputStream = this.schemaResource.getInputStream()) {
129+
try (InputStream inputStream = schemaResource.getInputStream()) {
128130
return new SchemaParser().parse(inputStream);
129131
}
130132
}
131133
catch (IOException ex) {
132-
throw new IllegalArgumentException("Failed to load resourceLocation " + this.schemaResource.toString());
134+
throw new IllegalArgumentException("Failed to load schema resource: " + schemaResource.toString());
133135
}
134136
}
135137

spring-graphql/src/main/java/org/springframework/graphql/execution/GraphQlSource.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,13 @@ static Builder builder() {
7070
interface Builder {
7171

7272
/**
73-
* Provide the resource for the GraphQL {@literal ".schema"} file to parse.
74-
* @param resource the resource for the GraphQL schema
73+
* Add {@literal ".graphqls"} schema resources to be
74+
* {@link TypeDefinitionRegistry#merge(TypeDefinitionRegistry) merged} into the type registry.
75+
* @param resources resources for the GraphQL schema
7576
* @return the current builder
7677
* @see graphql.schema.idl.SchemaParser#parse(File)
7778
*/
78-
Builder schemaResource(Resource resource);
79+
Builder schemaResources(Resource... resources);
7980

8081
/**
8182
* Set a {@link RuntimeWiring} to contribute data fetchers and more.

spring-graphql/src/test/java/org/springframework/graphql/GraphQlTestUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static GraphQlSource.Builder initGraphQlSource(String schemaContent, Stri
5757
.build();
5858

5959
return GraphQlSource.builder()
60-
.schemaResource(new ByteArrayResource(schemaContent.getBytes(StandardCharsets.UTF_8)))
60+
.schemaResources(new ByteArrayResource(schemaContent.getBytes(StandardCharsets.UTF_8)))
6161
.runtimeWiring(wiring);
6262
}
6363

spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private static GraphQlSource graphQlSource(Consumer<TypeRuntimeWiring.Builder> c
202202
configurer.accept(wiringBuilder);
203203
builder.type(wiringBuilder);
204204
return GraphQlSource.builder()
205-
.schemaResource(new ClassPathResource("books/schema.graphqls"))
205+
.schemaResources(new ClassPathResource("books/schema.graphqls"))
206206
.runtimeWiring(builder.build())
207207
.build();
208208
}

0 commit comments

Comments
 (0)