Auto-configure Spring GraphQL base infrastructure
This commit adds the auto-configuration for setting up the base Spring GraphQL infrastructure. Because GraphQL doesn't depend on any particular transport, we must have a separate configuration for creating: * the `GraphQlSource`, which holds the schema and the `GraphQL` instance * the `GraphQlService` for executing incoming requests * the `BatchLoaderRegistry` for batch loading support * the `AnnotatedControllerConfigurer` for supporting the annotated controllers programming model This comes with a starting point for the `"spring.graphql.*"` configuration properties; we can now configure the locations and file extensions of GraphQL schema files we should load and configure at startup. See gh-29140
This commit is contained in:
parent
e5e157528b
commit
de808834f5
|
|
@ -174,6 +174,7 @@ public class DocumentConfigurationProperties extends DefaultTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void webPrefixes(Config prefix) {
|
private void webPrefixes(Config prefix) {
|
||||||
|
prefix.accept("spring.graphql");
|
||||||
prefix.accept("spring.hateoas");
|
prefix.accept("spring.hateoas");
|
||||||
prefix.accept("spring.http");
|
prefix.accept("spring.http");
|
||||||
prefix.accept("spring.servlet");
|
prefix.accept("spring.servlet");
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ dependencies {
|
||||||
optional("org.springframework.data:spring-data-neo4j")
|
optional("org.springframework.data:spring-data-neo4j")
|
||||||
optional("org.springframework.data:spring-data-r2dbc")
|
optional("org.springframework.data:spring-data-r2dbc")
|
||||||
optional("org.springframework.data:spring-data-redis")
|
optional("org.springframework.data:spring-data-redis")
|
||||||
|
optional("org.springframework.graphql:spring-graphql")
|
||||||
optional("org.springframework.hateoas:spring-hateoas")
|
optional("org.springframework.hateoas:spring-hateoas")
|
||||||
optional("org.springframework.security:spring-security-acl")
|
optional("org.springframework.security:spring-security-acl")
|
||||||
optional("org.springframework.security:spring-security-config")
|
optional("org.springframework.security:spring-security-config")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
import graphql.execution.instrumentation.Instrumentation;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
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.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||||
|
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
||||||
|
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||||
|
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
|
||||||
|
import org.springframework.graphql.execution.ExecutionGraphQlService;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
import org.springframework.graphql.execution.MissingSchemaException;
|
||||||
|
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link EnableAutoConfiguration Auto-configuration} for creating a Spring GraphQL base
|
||||||
|
* infrastructure.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
|
||||||
|
@EnableConfigurationProperties(GraphQlProperties.class)
|
||||||
|
public class GraphQlAutoConfiguration {
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(GraphQlAutoConfiguration.class);
|
||||||
|
|
||||||
|
private final BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolver, GraphQlProperties properties,
|
||||||
|
ObjectProvider<DataFetcherExceptionResolver> exceptionResolversProvider,
|
||||||
|
ObjectProvider<Instrumentation> instrumentationsProvider,
|
||||||
|
ObjectProvider<RuntimeWiringConfigurer> wiringConfigurers,
|
||||||
|
ObjectProvider<GraphQlSourceBuilderCustomizer> sourceCustomizers) {
|
||||||
|
|
||||||
|
List<Resource> schemaResources = resolveSchemaResources(resourcePatternResolver,
|
||||||
|
properties.getSchema().getLocations(), properties.getSchema().getFileExtensions());
|
||||||
|
GraphQlSource.Builder builder = GraphQlSource.builder()
|
||||||
|
.schemaResources(schemaResources.toArray(new Resource[0]))
|
||||||
|
.exceptionResolvers(exceptionResolversProvider.orderedStream().collect(Collectors.toList()))
|
||||||
|
.instrumentation(instrumentationsProvider.orderedStream().collect(Collectors.toList()));
|
||||||
|
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
|
||||||
|
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||||
|
try {
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
catch (MissingSchemaException exc) {
|
||||||
|
throw new InvalidSchemaLocationsException(properties.getSchema().getLocations(), resourcePatternResolver,
|
||||||
|
exc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public BatchLoaderRegistry batchLoaderRegistry() {
|
||||||
|
return this.batchLoaderRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public GraphQlService graphQlService(GraphQlSource graphQlSource) {
|
||||||
|
ExecutionGraphQlService service = new ExecutionGraphQlService(graphQlSource);
|
||||||
|
service.addDataLoaderRegistrar(this.batchLoaderRegistry);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
|
||||||
|
AnnotatedControllerConfigurer annotatedControllerConfigurer = new AnnotatedControllerConfigurer();
|
||||||
|
annotatedControllerConfigurer.setConversionService(new DefaultFormattingConversionService());
|
||||||
|
return annotatedControllerConfigurer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String[] schemaLocations,
|
||||||
|
String[] fileExtensions) {
|
||||||
|
List<Resource> schemaResources = new ArrayList<>();
|
||||||
|
for (String location : schemaLocations) {
|
||||||
|
for (String extension : fileExtensions) {
|
||||||
|
String resourcePattern = location + "*" + extension;
|
||||||
|
try {
|
||||||
|
schemaResources.addAll(Arrays.asList(resolver.getResources(resourcePattern)));
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
logger.debug("Could not resolve schema location: '" + resourcePattern + "'", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schemaResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ConfigurationProperties properties} for Spring GraphQL.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(prefix = "spring.graphql")
|
||||||
|
public class GraphQlProperties {
|
||||||
|
|
||||||
|
private final Schema schema = new Schema();
|
||||||
|
|
||||||
|
public Schema getSchema() {
|
||||||
|
return this.schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Schema {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locations of GraphQL schema files.
|
||||||
|
*/
|
||||||
|
private String[] locations = new String[] { "classpath:graphql/**/" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File extensions for GraphQL schema files.
|
||||||
|
*/
|
||||||
|
private String[] fileExtensions = new String[] { ".graphqls", ".gqls" };
|
||||||
|
|
||||||
|
public String[] getLocations() {
|
||||||
|
return this.locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocations(String[] locations) {
|
||||||
|
this.locations = appendSlashIfNecessary(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getFileExtensions() {
|
||||||
|
return this.fileExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileExtensions(String[] fileExtensions) {
|
||||||
|
this.fileExtensions = fileExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] appendSlashIfNecessary(String[] locations) {
|
||||||
|
return Arrays.stream(locations).map((location) -> location.endsWith("/") ? location : location + "/")
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface that can be implemented by beans wishing to customize properties of
|
||||||
|
* {@link org.springframework.graphql.execution.GraphQlSource.Builder} whilst retaining
|
||||||
|
* default auto-configuration.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface GraphQlSourceBuilderCustomizer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customize the {@link GraphQlSource.Builder} instance.
|
||||||
|
* @param builder builder the builder to customize
|
||||||
|
*/
|
||||||
|
void customize(GraphQlSource.Builder builder);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.core.NestedRuntimeException;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link InvalidSchemaLocationsException} thrown when no schema file could be found in
|
||||||
|
* the provided locations.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
public class InvalidSchemaLocationsException extends NestedRuntimeException {
|
||||||
|
|
||||||
|
private final List<SchemaLocation> schemaLocations;
|
||||||
|
|
||||||
|
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver) {
|
||||||
|
this(locations, resolver, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidSchemaLocationsException(String[] locations, ResourcePatternResolver resolver, Throwable cause) {
|
||||||
|
super("No schema file could be found in the provided locations.", cause);
|
||||||
|
Assert.notEmpty(locations, "locations should not be empty");
|
||||||
|
Assert.notNull(resolver, "resolver should not be null");
|
||||||
|
List<SchemaLocation> providedLocations = new ArrayList<>();
|
||||||
|
for (String location : locations) {
|
||||||
|
try {
|
||||||
|
String uri = resolver.getResource(location).getURI().toASCIIString();
|
||||||
|
providedLocations.add(new SchemaLocation(location, uri));
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
providedLocations.add(new SchemaLocation(location, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.schemaLocations = Collections.unmodifiableList(providedLocations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of provided locations where to look for schemas.
|
||||||
|
* @return the list of locations
|
||||||
|
*/
|
||||||
|
public List<SchemaLocation> getSchemaLocations() {
|
||||||
|
return this.schemaLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location where to look for schemas.
|
||||||
|
*/
|
||||||
|
public static class SchemaLocation {
|
||||||
|
|
||||||
|
private final String location;
|
||||||
|
|
||||||
|
private final String uri;
|
||||||
|
|
||||||
|
SchemaLocation(String location, String uri) {
|
||||||
|
this.location = location;
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the location String to be resolved by a {@link ResourcePatternResolver}.
|
||||||
|
* @return the location
|
||||||
|
*/
|
||||||
|
public String getLocation() {
|
||||||
|
return this.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the resolved URI String for this location, an empty String if resolution
|
||||||
|
* failed.
|
||||||
|
* @return the resolved location or an empty String
|
||||||
|
*/
|
||||||
|
public String getUri() {
|
||||||
|
return this.uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-configuration for Spring GraphQL.
|
||||||
|
*/
|
||||||
|
package org.springframework.boot.autoconfigure.graphql;
|
||||||
|
|
@ -951,6 +951,14 @@
|
||||||
"level": "error"
|
"level": "error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spring.graphql.schema.locations",
|
||||||
|
"defaultValue": "classpath:graphql/**/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spring.graphql.schema.file-extensions",
|
||||||
|
"defaultValue": ".graphqls,.gqls"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spring.groovy.template.prefix",
|
"name": "spring.groovy.template.prefix",
|
||||||
"defaultValue": ""
|
"defaultValue": ""
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
|
||||||
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
|
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
|
||||||
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
|
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
|
||||||
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
|
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
|
||||||
|
org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration,\
|
||||||
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
|
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
|
||||||
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
|
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
|
||||||
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
|
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import graphql.GraphQL;
|
||||||
|
import graphql.execution.instrumentation.ChainedInstrumentation;
|
||||||
|
import graphql.execution.instrumentation.Instrumentation;
|
||||||
|
import graphql.schema.GraphQLSchema;
|
||||||
|
import graphql.schema.idl.RuntimeWiring;
|
||||||
|
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.graphql.GraphQlService;
|
||||||
|
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||||
|
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
||||||
|
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
|
||||||
|
import org.springframework.graphql.execution.GraphQlSource;
|
||||||
|
import org.springframework.graphql.execution.MissingSchemaException;
|
||||||
|
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link GraphQlAutoConfiguration}.
|
||||||
|
*/
|
||||||
|
class GraphQlAutoConfigurationTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContributeDefaultBeans() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(GraphQlSource.class);
|
||||||
|
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
|
||||||
|
assertThat(context).hasSingleBean(GraphQlService.class);
|
||||||
|
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void schemaShouldScanNestedFolders() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(GraphQlSource.class);
|
||||||
|
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
|
||||||
|
GraphQLSchema schema = graphQlSource.schema();
|
||||||
|
assertThat(schema.getObjectType("Book")).isNotNull();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWhenSchemaFileIsMissing() {
|
||||||
|
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:missing/").run((context) -> {
|
||||||
|
assertThat(context).hasFailed();
|
||||||
|
assertThat(context).getFailure().getRootCause().isInstanceOf(MissingSchemaException.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseProgrammaticallyDefinedBuilder() {
|
||||||
|
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
|
||||||
|
assertThat(context).hasBean("customGraphQlSourceBuilder");
|
||||||
|
assertThat(context).hasSingleBean(GraphQlSource.Builder.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldScanLocationsWithCustomExtension() {
|
||||||
|
this.contextRunner.withPropertyValues("spring.graphql.schema.file-extensions:.graphqls,.custom")
|
||||||
|
.run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(GraphQlSource.class);
|
||||||
|
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
|
||||||
|
GraphQLSchema schema = graphQlSource.schema();
|
||||||
|
assertThat(schema.getObjectType("Book")).isNotNull();
|
||||||
|
assertThat(schema.getObjectType("Person")).isNotNull();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldBackOffWithCustomGraphQlSource() {
|
||||||
|
this.contextRunner.withUserConfiguration(CustomGraphQlSourceConfiguration.class).run((context) -> {
|
||||||
|
assertThat(context).getBeanNames(GraphQlSource.class).containsOnly("customGraphQlSource");
|
||||||
|
assertThat(context).hasSingleBean(GraphQlProperties.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConfigureDataFetcherExceptionResolvers() {
|
||||||
|
this.contextRunner.withUserConfiguration(DataFetcherExceptionResolverConfiguration.class).run((context) -> {
|
||||||
|
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
|
||||||
|
GraphQL graphQL = graphQlSource.graphQl();
|
||||||
|
assertThat(graphQL.getQueryStrategy()).extracting("dataFetcherExceptionHandler")
|
||||||
|
.satisfies((exceptionHandler) -> assertThat(exceptionHandler.getClass().getName())
|
||||||
|
.endsWith("ExceptionResolversExceptionHandler"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConfigureInstrumentation() {
|
||||||
|
this.contextRunner.withUserConfiguration(InstrumentationConfiguration.class).run((context) -> {
|
||||||
|
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
|
||||||
|
Instrumentation customInstrumentation = context.getBean("customInstrumentation", Instrumentation.class);
|
||||||
|
GraphQL graphQL = graphQlSource.graphQl();
|
||||||
|
assertThat(graphQL).extracting("instrumentation").isInstanceOf(ChainedInstrumentation.class)
|
||||||
|
.extracting("instrumentations", InstanceOfAssertFactories.iterable(Instrumentation.class))
|
||||||
|
.contains(customInstrumentation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldApplyRuntimeWiringConfigurers() {
|
||||||
|
this.contextRunner.withUserConfiguration(RuntimeWiringConfigurerConfiguration.class).run((context) -> {
|
||||||
|
RuntimeWiringConfigurerConfiguration.CustomRuntimeWiringConfigurer configurer = context
|
||||||
|
.getBean(RuntimeWiringConfigurerConfiguration.CustomRuntimeWiringConfigurer.class);
|
||||||
|
assertThat(configurer.applied).isTrue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldApplyGraphQlSourceBuilderCustomizer() {
|
||||||
|
this.contextRunner.withUserConfiguration(GraphQlSourceBuilderCustomizerConfiguration.class).run((context) -> {
|
||||||
|
GraphQlSourceBuilderCustomizerConfiguration.CustomGraphQlSourceBuilderCustomizer customizer = context
|
||||||
|
.getBean(GraphQlSourceBuilderCustomizerConfiguration.CustomGraphQlSourceBuilderCustomizer.class);
|
||||||
|
assertThat(customizer.applied).isTrue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class CustomGraphQlBuilderConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
GraphQlSource.Builder customGraphQlSourceBuilder() {
|
||||||
|
return GraphQlSource.builder().schemaResources(new ClassPathResource("graphql/schema.graphqls"),
|
||||||
|
new ClassPathResource("graphql/types/book.graphqls"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class CustomGraphQlSourceConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
GraphQlSource customGraphQlSource() {
|
||||||
|
return mock(GraphQlSource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class DataFetcherExceptionResolverConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
DataFetcherExceptionResolver customDataFetcherExceptionResolver() {
|
||||||
|
return mock(DataFetcherExceptionResolver.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class InstrumentationConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
Instrumentation customInstrumentation() {
|
||||||
|
return mock(Instrumentation.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class RuntimeWiringConfigurerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
CustomRuntimeWiringConfigurer customRuntimeWiringConfigurer() {
|
||||||
|
return new CustomRuntimeWiringConfigurer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CustomRuntimeWiringConfigurer implements RuntimeWiringConfigurer {
|
||||||
|
|
||||||
|
public boolean applied = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(RuntimeWiring.Builder builder) {
|
||||||
|
this.applied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class GraphQlSourceBuilderCustomizerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
CustomGraphQlSourceBuilderCustomizer customGraphQlSourceBuilderCustomizer() {
|
||||||
|
return new CustomGraphQlSourceBuilderCustomizer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CustomGraphQlSourceBuilderCustomizer implements GraphQlSourceBuilderCustomizer {
|
||||||
|
|
||||||
|
public boolean applied = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(GraphQlSource.Builder builder) {
|
||||||
|
this.applied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.autoconfigure.graphql;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link InvalidSchemaLocationsException}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class InvalidSchemaLocationsExceptionTests {
|
||||||
|
|
||||||
|
private final String schemaFolder = "graphql/";
|
||||||
|
|
||||||
|
private final String[] locations = new String[] { "classpath:" + this.schemaFolder };
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectEmptyLocations() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(
|
||||||
|
() -> new InvalidSchemaLocationsException(new String[] {}, new PathMatchingResourcePatternResolver()))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class).withMessage("locations should not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectNullResolver() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new InvalidSchemaLocationsException(this.locations, null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class).withMessage("resolver should not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeConfiguredLocations() {
|
||||||
|
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(this.locations,
|
||||||
|
new PathMatchingResourcePatternResolver());
|
||||||
|
assertThat(exception.getSchemaLocations()).hasSize(1);
|
||||||
|
InvalidSchemaLocationsException.SchemaLocation schemaLocation = exception.getSchemaLocations().get(0);
|
||||||
|
assertThat(schemaLocation.getLocation()).isEqualTo(this.locations[0]);
|
||||||
|
assertThat(schemaLocation.getUri()).endsWith(this.schemaFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFailWithUnresolvableLocations() {
|
||||||
|
String unresolved = "classpath:unresolved/";
|
||||||
|
InvalidSchemaLocationsException exception = new InvalidSchemaLocationsException(new String[] { unresolved },
|
||||||
|
new PathMatchingResourcePatternResolver());
|
||||||
|
assertThat(exception.getSchemaLocations()).hasSize(1);
|
||||||
|
InvalidSchemaLocationsException.SchemaLocation schemaLocation = exception.getSchemaLocations().get(0);
|
||||||
|
assertThat(schemaLocation.getLocation()).isEqualTo(unresolved);
|
||||||
|
assertThat(schemaLocation.getUri()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
type Query {
|
||||||
|
greeting(name: String! = "Spring"): String!
|
||||||
|
bookById(id: ID): Book
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
type Book {
|
||||||
|
id: ID
|
||||||
|
name: String
|
||||||
|
pageCount: Int
|
||||||
|
author: String
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
type Person {
|
||||||
|
id: ID
|
||||||
|
name: String
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue