Reinstate Spring for GraphQL auto-configuration

This commit adds the Spring for GraphQL auto-configuration back
into Spring Boot 3.0, now that a 1.1.0 release is scheduled with the
required baseline. This release also needs GraphQL Java 19.0 as a
baseline.

Closes gh-31809
This commit is contained in:
Brian Clozel
2022-07-20 13:57:17 +02:00
parent ab469e8b6a
commit 38f1bc9793
125 changed files with 7728 additions and 4 deletions

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2022 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.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that only matches when a GraphQL schema is defined for
* the application, via schema files or infrastructure beans.
*
* @author Brian Clozel
* @since 2.7.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(DefaultGraphQlSchemaCondition.class)
public @interface ConditionalOnGraphQlSchema {
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2012-2022 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.Collections;
import java.util.List;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link Condition} that checks whether a GraphQL schema has been defined in the
* application. This is looking for:
* <ul>
* <li>schema files in the {@link GraphQlProperties configured locations}</li>
* <li>or infrastructure beans such as {@link GraphQlSourceBuilderCustomizer}</li>
* </ul>
*
* @author Brian Clozel
* @see ConditionalOnGraphQlSchema
*/
class DefaultGraphQlSchemaCondition extends SpringBootCondition implements ConfigurationCondition {
@Override
public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() {
return ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean match = false;
List<ConditionMessage> messages = new ArrayList<>(2);
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnGraphQlSchema.class);
Binder binder = Binder.get(context.getEnvironment());
GraphQlProperties.Schema schema = binder.bind("spring.graphql.schema", GraphQlProperties.Schema.class)
.orElse(new GraphQlProperties.Schema());
ResourcePatternResolver resourcePatternResolver = ResourcePatternUtils
.getResourcePatternResolver(context.getResourceLoader());
List<Resource> schemaResources = resolveSchemaResources(resourcePatternResolver, schema.getLocations(),
schema.getFileExtensions());
if (!schemaResources.isEmpty()) {
match = true;
messages.add(message.found("schema", "schemas").items(ConditionMessage.Style.QUOTE, schemaResources));
}
else {
messages.add(message.didNotFind("schema files in locations").items(ConditionMessage.Style.QUOTE,
Arrays.asList(schema.getLocations())));
}
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
String[] customizerBeans = beanFactory.getBeanNamesForType(GraphQlSourceBuilderCustomizer.class, false, false);
if (customizerBeans.length != 0) {
match = true;
messages.add(message.found("customizer", "customizers").items(Arrays.asList(customizerBeans)));
}
else {
messages.add((message.didNotFind("GraphQlSourceBuilderCustomizer").atAll()));
}
return new ConditionOutcome(match, ConditionMessage.of(messages));
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String[] locations,
String[] extensions) {
List<Resource> resources = new ArrayList<>();
for (String location : locations) {
for (String extension : extensions) {
resources.addAll(resolveSchemaResources(resolver, location + "*" + extension));
}
}
return resources;
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String pattern) {
try {
return Arrays.asList(resolver.getResources(pattern));
}
catch (IOException ex) {
return Collections.emptyList();
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2012-2022 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.Collections;
import java.util.List;
import java.util.stream.Collectors;
import graphql.GraphQL;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.idl.RuntimeWiring.Builder;
import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.log.LogMessage;
import org.springframework.graphql.ExecutionGraphQlService;
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.DefaultExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.SubscriptionExceptionResolver;
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a Spring GraphQL base
* infrastructure.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
@ConditionalOnGraphQlSchema
@EnableConfigurationProperties(GraphQlProperties.class)
public class GraphQlAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlAutoConfiguration.class);
private final ListableBeanFactory beanFactory;
public GraphQlAutoConfiguration(ListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Bean
@ConditionalOnMissingBean
public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolver, GraphQlProperties properties,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolvers,
ObjectProvider<SubscriptionExceptionResolver> subscriptionExceptionResolvers,
ObjectProvider<Instrumentation> instrumentations, ObjectProvider<RuntimeWiringConfigurer> wiringConfigurers,
ObjectProvider<GraphQlSourceBuilderCustomizer> sourceCustomizers) {
String[] schemaLocations = properties.getSchema().getLocations();
Resource[] schemaResources = resolveSchemaResources(resourcePatternResolver, schemaLocations,
properties.getSchema().getFileExtensions());
GraphQlSource.SchemaResourceBuilder builder = GraphQlSource.schemaResourceBuilder()
.schemaResources(schemaResources).exceptionResolvers(toList(exceptionResolvers))
.subscriptionExceptionResolvers(toList(subscriptionExceptionResolvers))
.instrumentation(toList(instrumentations));
if (!properties.getSchema().getIntrospection().isEnabled()) {
builder.configureRuntimeWiring(this::enableIntrospection);
}
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
private Builder enableIntrospection(Builder wiring) {
return wiring.fieldVisibility(NoIntrospectionGraphqlFieldVisibility.NO_INTROSPECTION_FIELD_VISIBILITY);
}
private Resource[] resolveSchemaResources(ResourcePatternResolver resolver, String[] locations,
String[] extensions) {
List<Resource> resources = new ArrayList<>();
for (String location : locations) {
for (String extension : extensions) {
resources.addAll(resolveSchemaResources(resolver, location + "*" + extension));
}
}
return resources.toArray(new Resource[0]);
}
private List<Resource> resolveSchemaResources(ResourcePatternResolver resolver, String pattern) {
try {
return Arrays.asList(resolver.getResources(pattern));
}
catch (IOException ex) {
logger.debug(LogMessage.format("Could not resolve schema location: '%s'", pattern), ex);
return Collections.emptyList();
}
}
@Bean
@ConditionalOnMissingBean
public BatchLoaderRegistry batchLoaderRegistry() {
return new DefaultBatchLoaderRegistry();
}
@Bean
@ConditionalOnMissingBean
public ExecutionGraphQlService executionGraphQlService(GraphQlSource graphQlSource,
BatchLoaderRegistry batchLoaderRegistry) {
DefaultExecutionGraphQlService service = new DefaultExecutionGraphQlService(graphQlSource);
service.addDataLoaderRegistrar(batchLoaderRegistry);
return service;
}
@Bean
@ConditionalOnMissingBean
public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
AnnotatedControllerConfigurer controllerConfigurer = new AnnotatedControllerConfigurer();
controllerConfigurer
.addFormatterRegistrar((registry) -> ApplicationConversionService.addBeans(registry, this.beanFactory));
return controllerConfigurer;
}
private <T> List<T> toList(ObjectProvider<T> provider) {
return provider.orderedStream().collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2012-2022 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.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
/**
* Configuration properties for GraphQL endpoint's CORS support.
*
* @author Andy Wilkinson
* @author Brian Clozel
* @since 2.7.0
*/
@ConfigurationProperties(prefix = "spring.graphql.cors")
public class GraphQlCorsProperties {
/**
* Comma-separated list of origins to allow with '*' allowing all origins. When
* allow-credentials is enabled, '*' cannot be used, and setting origin patterns
* should be considered instead. When neither allowed origins nor allowed origin
* patterns are set, cross-origin requests are effectively disabled.
*/
private List<String> allowedOrigins = new ArrayList<>();
/**
* Comma-separated list of origin patterns to allow. Unlike allowed origins which only
* support '*', origin patterns are more flexible, e.g. 'https://*.example.com', and
* can be used with allow-credentials. When neither allowed origins nor allowed origin
* patterns are set, cross-origin requests are effectively disabled.
*/
private List<String> allowedOriginPatterns = new ArrayList<>();
/**
* Comma-separated list of HTTP methods to allow. '*' allows all methods. When not
* set, defaults to GET.
*/
private List<String> allowedMethods = new ArrayList<>();
/**
* Comma-separated list of HTTP headers to allow in a request. '*' allows all headers.
*/
private List<String> allowedHeaders = new ArrayList<>();
/**
* Comma-separated list of headers to include in a response.
*/
private List<String> exposedHeaders = new ArrayList<>();
/**
* Whether credentials are supported. When not set, credentials are not supported.
*/
private Boolean allowCredentials;
/**
* How long the response from a pre-flight request can be cached by clients. If a
* duration suffix is not specified, seconds will be used.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration maxAge = Duration.ofSeconds(1800);
public List<String> getAllowedOrigins() {
return this.allowedOrigins;
}
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
public List<String> getAllowedOriginPatterns() {
return this.allowedOriginPatterns;
}
public void setAllowedOriginPatterns(List<String> allowedOriginPatterns) {
this.allowedOriginPatterns = allowedOriginPatterns;
}
public List<String> getAllowedMethods() {
return this.allowedMethods;
}
public void setAllowedMethods(List<String> allowedMethods) {
this.allowedMethods = allowedMethods;
}
public List<String> getAllowedHeaders() {
return this.allowedHeaders;
}
public void setAllowedHeaders(List<String> allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}
public List<String> getExposedHeaders() {
return this.exposedHeaders;
}
public void setExposedHeaders(List<String> exposedHeaders) {
this.exposedHeaders = exposedHeaders;
}
public Boolean getAllowCredentials() {
return this.allowCredentials;
}
public void setAllowCredentials(Boolean allowCredentials) {
this.allowCredentials = allowCredentials;
}
public Duration getMaxAge() {
return this.maxAge;
}
public void setMaxAge(Duration maxAge) {
this.maxAge = maxAge;
}
public CorsConfiguration toCorsConfiguration() {
if (CollectionUtils.isEmpty(this.allowedOrigins) && CollectionUtils.isEmpty(this.allowedOriginPatterns)) {
return null;
}
PropertyMapper map = PropertyMapper.get();
CorsConfiguration config = new CorsConfiguration();
map.from(this::getAllowedOrigins).to(config::setAllowedOrigins);
map.from(this::getAllowedOriginPatterns).to(config::setAllowedOriginPatterns);
map.from(this::getAllowedHeaders).whenNot(CollectionUtils::isEmpty).to(config::setAllowedHeaders);
map.from(this::getAllowedMethods).whenNot(CollectionUtils::isEmpty).to(config::setAllowedMethods);
map.from(this::getExposedHeaders).whenNot(CollectionUtils::isEmpty).to(config::setExposedHeaders);
map.from(this::getMaxAge).whenNonNull().as(Duration::getSeconds).to(config::setMaxAge);
map.from(this::getAllowCredentials).whenNonNull().to(config::setAllowCredentials);
return config;
}
}

View File

@@ -0,0 +1,230 @@
/*
* Copyright 2012-2022 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.time.Duration;
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 {
/**
* Path at which to expose a GraphQL request HTTP endpoint.
*/
private String path = "/graphql";
private final Graphiql graphiql = new Graphiql();
private final Schema schema = new Schema();
private final Websocket websocket = new Websocket();
private final Rsocket rsocket = new Rsocket();
public Graphiql getGraphiql() {
return this.graphiql;
}
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public Schema getSchema() {
return this.schema;
}
public Websocket getWebsocket() {
return this.websocket;
}
public Rsocket getRsocket() {
return this.rsocket;
}
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" };
private final Introspection introspection = new Introspection();
private final Printer printer = new Printer();
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);
}
public Introspection getIntrospection() {
return this.introspection;
}
public Printer getPrinter() {
return this.printer;
}
public static class Introspection {
/**
* Whether field introspection should be enabled at the schema level.
*/
private boolean enabled = true;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
public static class Printer {
/**
* Whether the endpoint that prints the schema is enabled. Schema is available
* under spring.graphql.path + "/schema".
*/
private boolean enabled = false;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
public static class Graphiql {
/**
* Path to the GraphiQL UI endpoint.
*/
private String path = "/graphiql";
/**
* Whether the default GraphiQL UI is enabled.
*/
private boolean enabled = false;
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
public static class Websocket {
/**
* Path of the GraphQL WebSocket subscription endpoint.
*/
private String path;
/**
* Time within which the initial {@code CONNECTION_INIT} type message must be
* received.
*/
private Duration connectionInitTimeout = Duration.ofSeconds(60);
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public Duration getConnectionInitTimeout() {
return this.connectionInitTimeout;
}
public void setConnectionInitTimeout(Duration connectionInitTimeout) {
this.connectionInitTimeout = connectionInitTimeout;
}
}
public static class Rsocket {
/**
* Mapping of the RSocket message handler.
*/
private String mapping;
public String getMapping() {
return this.mapping;
}
public void setMapping(String mapping) {
this.mapping = mapping;
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2022 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.SchemaResourceBuilder
* Builder} whilst retaining default auto-configuration.
*
* @author Rossen Stoyanchev
* @since 2.7.0
*/
@FunctionalInterface
public interface GraphQlSourceBuilderCustomizer {
/**
* Customize the
* {@link org.springframework.graphql.execution.GraphQlSource.SchemaResourceBuilder
* Builder} instance.
* @param builder builder the builder to customize
*/
void customize(GraphQlSource.SchemaResourceBuilder builder);
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2022 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.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories with Query
* By Example support and register them as {@code DataFetcher}s for any queries with a
* matching return type.
*
* @author Rossen Stoyanchev
* @since 2.7.0
* @see QueryByExampleDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QueryByExampleDataFetcher.class, QueryByExampleExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlQueryByExampleAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer queryByExampleRegistrar(ObjectProvider<QueryByExampleExecutor<?>> executors,
ObjectProvider<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QueryByExampleDataFetcher::autoRegistrationConfigurer,
executors, reactiveExecutors);
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2022 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.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.graphql.data.query.QuerydslDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories with
* Querydsl support and register them as {@code DataFetcher}s for any queries with a
* matching return type.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 2.7.0
* @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, QuerydslPredicateExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlQuerydslAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer querydslRegistrar(ObjectProvider<QuerydslPredicateExecutor<?>> executors,
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QuerydslDataFetcher::autoRegistrationConfigurer, executors,
reactiveExecutors);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2022 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.data;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
/**
* {@link GraphQlSourceBuilderCustomizer} to apply auto-configured QueryDSL
* {@link RuntimeWiringConfigurer RuntimeWiringConfigurers}.
*
* @param <E> the executor type
* @param <R> the reactive executor type
* @author Phillip Webb
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
class GraphQlQuerydslSourceBuilderCustomizer<E, R> implements GraphQlSourceBuilderCustomizer {
private final BiFunction<List<E>, List<R>, RuntimeWiringConfigurer> wiringConfigurerFactory;
private final List<E> executors;
private final List<R> reactiveExecutors;
GraphQlQuerydslSourceBuilderCustomizer(
BiFunction<List<E>, List<R>, RuntimeWiringConfigurer> wiringConfigurerFactory, ObjectProvider<E> executors,
ObjectProvider<R> reactiveExecutors) {
this(wiringConfigurerFactory, toList(executors), toList(reactiveExecutors));
}
GraphQlQuerydslSourceBuilderCustomizer(
BiFunction<List<E>, List<R>, RuntimeWiringConfigurer> wiringConfigurerFactory, List<E> executors,
List<R> reactiveExecutors) {
this.wiringConfigurerFactory = wiringConfigurerFactory;
this.executors = executors;
this.reactiveExecutors = reactiveExecutors;
}
@Override
public void customize(GraphQlSource.SchemaResourceBuilder builder) {
if (!this.executors.isEmpty() || !this.reactiveExecutors.isEmpty()) {
builder.configureRuntimeWiring(this.wiringConfigurerFactory.apply(this.executors, this.reactiveExecutors));
}
}
private static <T> List<T> toList(ObjectProvider<T> provider) {
return (provider != null) ? provider.orderedStream().collect(Collectors.toList()) : Collections.emptyList();
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2022 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.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories with Query
* By Example support and register them as {@code DataFetcher}s for any queries with a
* matching return type.
*
* @author Rossen Stoyanchev
* @since 2.7.0
* @see QueryByExampleDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QueryByExampleDataFetcher.class, ReactiveQueryByExampleExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlReactiveQueryByExampleAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer reactiveQueryByExampleRegistrar(
ObjectProvider<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QueryByExampleDataFetcher::autoRegistrationConfigurer,
(ObjectProvider<QueryByExampleExecutor<?>>) null, reactiveExecutors);
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2022 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.data;
import java.util.List;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.graphql.data.query.QuerydslDataFetcher;
import org.springframework.graphql.execution.GraphQlSource;
/**
* {@link EnableAutoConfiguration Auto-configuration} that creates a
* {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories with
* Querydsl support and register them as {@code DataFetcher}s for any queries with a
* matching return type.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 2.7.0
* @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List)
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, ReactiveQuerydslPredicateExecutor.class })
@ConditionalOnBean(GraphQlSource.class)
public class GraphQlReactiveQuerydslAutoConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer reactiveQuerydslRegistrar(
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
return new GraphQlQuerydslSourceBuilderCustomizer<>(QuerydslDataFetcher::autoRegistrationConfigurer,
(ObjectProvider<QuerydslPredicateExecutor<?>>) null, reactiveExecutors);
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Auto-configuration classes for data integrations with GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.data;

View File

@@ -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;

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2012-2022 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.reactive;
import java.util.Collections;
import java.util.stream.Collectors;
import graphql.GraphQL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.log.LogMessage;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
import org.springframework.graphql.server.webflux.GraphiQlHandler;
import org.springframework.graphql.server.webflux.SchemaHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.server.support.WebSocketUpgradeHandlerPredicate;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* WebFlux.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
@ConditionalOnBean(ExecutionGraphQlService.class)
@EnableConfigurationProperties(GraphQlCorsProperties.class)
public class GraphQlWebFluxAutoConfiguration {
private static final RequestPredicate SUPPORTS_MEDIATYPES = accept(MediaType.APPLICATION_GRAPHQL,
MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON));
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
return new GraphQlHttpHandler(webGraphQlHandler);
}
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder = builder.GET(path, this::onlyAllowPost);
builder = builder.POST(path, SUPPORTS_MEDIATYPES, httpHandler::handleRequest);
if (properties.getGraphiql().isEnabled()) {
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder = builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest);
}
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder = builder.GET(path + "/schema", schemaHandler::handleRequest);
}
return builder.build();
}
private Mono<ServerResponse> onlyAllowPost(ServerRequest request) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED).headers(this::onlyAllowPost).build();
}
private void onlyAllowPost(HttpHeaders headers) {
headers.setAllow(Collections.singleton(HttpMethod.POST));
}
@Configuration(proxyBeanMethods = false)
public static class GraphQlEndpointCorsConfiguration implements WebFluxConfigurer {
final GraphQlProperties graphQlProperties;
final GraphQlCorsProperties corsProperties;
public GraphQlEndpointCorsConfiguration(GraphQlProperties graphQlProps, GraphQlCorsProperties corsProps) {
this.graphQlProperties = graphQlProps;
this.corsProperties = corsProps;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration);
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
public static class WebSocketConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, ServerCodecConfigurer configurer) {
return new GraphQlWebSocketHandler(webGraphQlHandler, configurer,
properties.getWebsocket().getConnectionInitTimeout());
}
@Bean
public HandlerMapping graphQlWebSocketEndpoint(GraphQlWebSocketHandler graphQlWebSocketHandler,
GraphQlProperties properties) {
String path = properties.getWebsocket().getPath();
logger.info(LogMessage.format("GraphQL endpoint WebSocket %s", path));
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setHandlerPredicate(new WebSocketUpgradeHandlerPredicate());
mapping.setUrlMap(Collections.singletonMap(path, graphQlWebSocketHandler));
mapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
return mapping;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Auto-configuration classes for WebFlux support in Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.reactive;

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2022 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.rsocket;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.GraphQL;
import io.rsocket.core.RSocketServer;
import reactor.netty.http.server.HttpServer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.graphql.server.RSocketGraphQlInterceptor;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* RSocket.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = { GraphQlAutoConfiguration.class, RSocketMessagingAutoConfiguration.class })
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class })
@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class })
@ConditionalOnProperty(prefix = "spring.graphql.rsocket", name = "mapping")
public class GraphQlRSocketAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService,
ObjectProvider<RSocketGraphQlInterceptor> interceptorsProvider, ObjectMapper objectMapper) {
List<RSocketGraphQlInterceptor> interceptors = interceptorsProvider.orderedStream()
.collect(Collectors.toList());
return new GraphQlRSocketHandler(graphQlService, interceptors, new Jackson2JsonEncoder(objectMapper));
}
@Bean
@ConditionalOnMissingBean
public GraphQlRSocketController graphQlRSocketController(GraphQlRSocketHandler handler) {
return new GraphQlRSocketController(handler);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2022 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.rsocket;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
@Controller
class GraphQlRSocketController {
private final GraphQlRSocketHandler handler;
GraphQlRSocketController(GraphQlRSocketHandler handler) {
this.handler = handler;
}
@MessageMapping("${spring.graphql.rsocket.mapping}")
Mono<Map<String, Object>> handle(Map<String, Object> payload) {
return this.handler.handle(payload);
}
@MessageMapping("${spring.graphql.rsocket.mapping}")
Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
return this.handler.handleSubscription(payload);
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2022 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.rsocket;
import graphql.GraphQL;
import io.rsocket.RSocket;
import io.rsocket.transport.netty.client.TcpClientTransport;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.graphql.client.RSocketGraphQlClient;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.util.MimeTypeUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link RSocketGraphQlClient}.
* This auto-configuration creates
* {@link org.springframework.graphql.client.RSocketGraphQlClient.Builder
* RSocketGraphQlClient.Builder} prototype beans, as the builders are stateful and should
* not be reused to build client instances with different configurations.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = RSocketRequesterAutoConfiguration.class)
@ConditionalOnClass({ GraphQL.class, RSocketGraphQlClient.class, RSocketRequester.class, RSocket.class,
TcpClientTransport.class })
public class RSocketGraphQlClientAutoConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public RSocketGraphQlClient.Builder<?> rsocketGraphQlClientBuilder(
RSocketRequester.Builder rsocketRequesterBuilder) {
return RSocketGraphQlClient.builder(rsocketRequesterBuilder.dataMimeType(MimeTypeUtils.APPLICATION_JSON));
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2020-2022 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 classes for RSocket integration with GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2022 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.security;
import graphql.GraphQL;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.execution.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for
* Spring GraphQL with WebFlux.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = GraphQlWebFluxAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebFluxSecurity.class })
@ConditionalOnBean(GraphQlHttpHandler.class)
public class GraphQlWebFluxSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactiveSecurityDataFetcherExceptionResolver reactiveSecurityDataFetcherExceptionResolver() {
return new ReactiveSecurityDataFetcherExceptionResolver();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2022 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.security;
import graphql.GraphQL;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.execution.SecurityContextThreadLocalAccessor;
import org.springframework.graphql.execution.SecurityDataFetcherExceptionResolver;
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Security support for
* Spring GraphQL with MVC.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = GraphQlWebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class, EnableWebSecurity.class })
@ConditionalOnBean(GraphQlHttpHandler.class)
public class GraphQlWebMvcSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SecurityDataFetcherExceptionResolver securityDataFetcherExceptionResolver() {
return new SecurityDataFetcherExceptionResolver();
}
@Bean
@ConditionalOnMissingBean
public SecurityContextThreadLocalAccessor securityContextThreadLocalAccessor() {
return new SecurityContextThreadLocalAccessor();
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Auto-configuration classes for Security support in Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.security;

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2012-2022 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.servlet;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import graphql.GraphQL;
import jakarta.websocket.server.ServerContainer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.log.LogMessage;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
import org.springframework.graphql.server.webmvc.GraphQlWebSocketHandler;
import org.springframework.graphql.server.webmvc.GraphiQlHandler;
import org.springframework.graphql.server.webmvc.SchemaHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* Spring MVC.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
@ConditionalOnBean(ExecutionGraphQlService.class)
@EnableConfigurationProperties(GraphQlCorsProperties.class)
public class GraphQlWebMvcAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class);
private static MediaType[] SUPPORTED_MEDIA_TYPES = new MediaType[] { MediaType.APPLICATION_GRAPHQL,
MediaType.APPLICATION_JSON };
@Bean
@ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
return new GraphQlHttpHandler(webGraphQlHandler);
}
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptorsProvider,
ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList()))
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder = builder.GET(path, this::onlyAllowPost);
builder = builder.POST(path, RequestPredicates.contentType(SUPPORTED_MEDIA_TYPES)
.and(RequestPredicates.accept(SUPPORTED_MEDIA_TYPES)), httpHandler::handleRequest);
if (properties.getGraphiql().isEnabled()) {
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);
}
if (properties.getSchema().getPrinter().isEnabled()) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder = builder.GET(path + "/schema", schemaHandler::handleRequest);
}
return builder.build();
}
private ServerResponse onlyAllowPost(ServerRequest request) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED).headers(this::onlyAllowPost).build();
}
private void onlyAllowPost(HttpHeaders headers) {
headers.setAllow(Collections.singleton(HttpMethod.POST));
}
@Configuration(proxyBeanMethods = false)
public static class GraphQlEndpointCorsConfiguration implements WebMvcConfigurer {
final GraphQlProperties graphQlProperties;
final GraphQlCorsProperties corsProperties;
public GraphQlEndpointCorsConfiguration(GraphQlProperties graphQlProps, GraphQlCorsProperties corsProps) {
this.graphQlProperties = graphQlProps;
this.corsProperties = corsProps;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration);
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ServerContainer.class, WebSocketHandler.class })
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
public static class WebSocketConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
GraphQlProperties properties, HttpMessageConverters converters) {
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
properties.getWebsocket().getConnectionInitTimeout());
}
private GenericHttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) {
return converters.getConverters().stream().filter(this::canReadJsonMap).findFirst()
.map(this::asGenericHttpMessageConverter)
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
}
private boolean canReadJsonMap(HttpMessageConverter<?> candidate) {
return candidate.canRead(Map.class, MediaType.APPLICATION_JSON);
}
@SuppressWarnings("unchecked")
private GenericHttpMessageConverter<Object> asGenericHttpMessageConverter(HttpMessageConverter<?> converter) {
return (GenericHttpMessageConverter<Object>) converter;
}
@Bean
public HandlerMapping graphQlWebSocketMapping(GraphQlWebSocketHandler handler, GraphQlProperties properties) {
String path = properties.getWebsocket().getPath();
logger.info(LogMessage.format("GraphQL endpoint WebSocket %s", path));
WebSocketHandlerMapping mapping = new WebSocketHandlerMapping();
mapping.setWebSocketUpgradeMatch(true);
mapping.setUrlMap(Collections.singletonMap(path,
handler.asWebSocketHttpRequestHandler(new DefaultHandshakeHandler())));
mapping.setOrder(2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
return mapping;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Auto-configuration classes for MVC support in Spring GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.servlet;

View File

@@ -1162,6 +1162,14 @@
"level": "error"
}
},
{
"name": "spring.graphql.schema.file-extensions",
"defaultValue": ".graphqls,.gqls"
},
{
"name": "spring.graphql.schema.locations",
"defaultValue": "classpath:graphql/**/"
},
{
"name": "spring.groovy.template.prefix",
"defaultValue": ""
@@ -2422,6 +2430,56 @@
}
]
},
{
"name": "spring.datasource.xa.data-source-class-name",
"providers": [
{
"name": "class-reference",
"parameters": {
"target": "javax.sql.XADataSource"
}
}
]
},
{
"name": "spring.graphql.cors.allowed-headers",
"values": [
{
"value": "*"
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.graphql.cors.allowed-methods",
"values": [
{
"value": "*"
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.graphql.cors.allowed-origins",
"values": [
{
"value": "*"
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.jmx.server",
"providers": [

View File

@@ -44,6 +44,17 @@ org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAuto
org.springframework.boot.autoconfigure.elasticsearch.ReactiveElasticsearchClientAutoConfiguration
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration
org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlReactiveQueryByExampleAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlReactiveQuerydslAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlQueryByExampleAutoConfiguration
org.springframework.boot.autoconfigure.graphql.data.GraphQlQuerydslAutoConfiguration
org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.graphql.rsocket.GraphQlRSocketAutoConfiguration
org.springframework.boot.autoconfigure.graphql.rsocket.RSocketGraphQlClientAutoConfiguration
org.springframework.boot.autoconfigure.graphql.security.GraphQlWebFluxSecurityAutoConfiguration
org.springframework.boot.autoconfigure.graphql.security.GraphQlWebMvcSecurityAutoConfiguration
org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration

View File

@@ -0,0 +1,79 @@
/*
* 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.data.annotation.Id;
/**
* Sample class for
*
* @author Brian Clozel
*/
public class Book {
@Id
String id;
String name;
int pageCount;
String author;
public Book() {
}
public Book(String id, String name, int pageCount, String author) {
this.id = id;
this.name = name;
this.pageCount = pageCount;
this.author = author;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getPageCount() {
return this.pageCount;
}
public void setPageCount(int pageCount) {
this.pageCount = pageCount;
}
public String getAuthor() {
return this.author;
}
public void setAuthor(String author) {
this.author = author;
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2012-2022 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.Collection;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConditionalOnGraphQlSchema}.
*
* @author Brian Clozel
*/
class DefaultGraphQlSchemaConditionTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
@Test
void matchesWhenSchemaFilesAreDetected() {
this.contextRunner.withUserConfiguration(TestingConfiguration.class).run((context) -> {
didMatch(context);
assertThat(conditionReportMessage(context)).contains("@ConditionalOnGraphQlSchema found schemas")
.contains("@ConditionalOnGraphQlSchema did not find GraphQlSourceBuilderCustomizer");
});
}
@Test
void matchesWhenCustomizerIsDetected() {
this.contextRunner.withUserConfiguration(CustomCustomizerConfiguration.class, TestingConfiguration.class)
.withPropertyValues("spring.graphql.schema.locations=classpath:graphql/missing").run((context) -> {
didMatch(context);
assertThat(conditionReportMessage(context)).contains(
"@ConditionalOnGraphQlSchema did not find schema files in locations 'classpath:graphql/missing/'")
.contains("@ConditionalOnGraphQlSchema found customizer myBuilderCuystomizer");
});
}
@Test
void doesNotMatchWhenBothAreMissing() {
this.contextRunner.withUserConfiguration(TestingConfiguration.class)
.withPropertyValues("spring.graphql.schema.locations=classpath:graphql/missing").run((context) -> {
assertThat(context).doesNotHaveBean("success");
assertThat(conditionReportMessage(context)).contains(
"@ConditionalOnGraphQlSchema did not find schema files in locations 'classpath:graphql/missing/'")
.contains("@ConditionalOnGraphQlSchema did not find GraphQlSourceBuilderCustomizer");
});
}
private void didMatch(AssertableApplicationContext context) {
assertThat(context).hasBean("success");
assertThat(context.getBean("success")).isEqualTo("success");
}
private String conditionReportMessage(AssertableApplicationContext context) {
Collection<ConditionEvaluationReport.ConditionAndOutcomes> conditionAndOutcomes = ConditionEvaluationReport
.get(context.getSourceApplicationContext().getBeanFactory()).getConditionAndOutcomesBySource().values();
return conditionAndOutcomes.iterator().next().iterator().next().getOutcome().getMessage();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnGraphQlSchema
static class TestingConfiguration {
@Bean
String success() {
return "success";
}
}
@Configuration(proxyBeanMethods = false)
static class CustomCustomizerConfiguration {
@Bean
GraphQlSourceBuilderCustomizer myBuilderCuystomizer() {
return (builder) -> {
};
}
}
}

View File

@@ -0,0 +1,270 @@
/*
* Copyright 2012-2022 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.nio.charset.StandardCharsets;
import graphql.GraphQL;
import graphql.execution.instrumentation.ChainedInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.visibility.DefaultGraphqlFieldVisibility;
import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility;
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.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.ExecutionGraphQlService;
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.DataLoaderRegistrar;
import org.springframework.graphql.execution.GraphQlSource;
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(ExecutionGraphQlService.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 shouldBackoffWhenSchemaFileIsMissing() {
this.contextRunner.withPropertyValues("spring.graphql.schema.locations:classpath:missing/")
.run((context) -> assertThat(context).hasNotFailed().doesNotHaveBean(GraphQlSource.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();
});
}
@Test
void fieldIntrospectionShouldBeEnabledByDefault() {
this.contextRunner.run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getCodeRegistry().getFieldVisibility()).isInstanceOf(DefaultGraphqlFieldVisibility.class);
});
}
@Test
void shouldDisableFieldIntrospection() {
this.contextRunner.withPropertyValues("spring.graphql.schema.introspection.enabled:false").run((context) -> {
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
GraphQLSchema schema = graphQlSource.schema();
assertThat(schema.getCodeRegistry().getFieldVisibility())
.isInstanceOf(NoIntrospectionGraphqlFieldVisibility.class);
});
}
@Test
void shouldConfigureCustomBatchLoaderRegistry() {
this.contextRunner
.withBean("customBatchLoaderRegistry", BatchLoaderRegistry.class, () -> mock(BatchLoaderRegistry.class))
.run((context) -> {
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
assertThat(context.getBean("customBatchLoaderRegistry"))
.isSameAs(context.getBean(BatchLoaderRegistry.class));
assertThat(context.getBean(ExecutionGraphQlService.class))
.extracting("dataLoaderRegistrars",
InstanceOfAssertFactories.list(DataLoaderRegistrar.class))
.containsOnly(context.getBean(BatchLoaderRegistry.class));
});
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlBuilderConfiguration {
@Bean
GraphQlSource.SchemaResourceBuilder customGraphQlSourceBuilder() {
return GraphQlSource.schemaResourceBuilder().schemaResources(
new ClassPathResource("graphql/schema.graphqls"),
new ClassPathResource("graphql/types/book.graphqls"));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomGraphQlSourceConfiguration {
@Bean
GraphQlSource customGraphQlSource() {
ByteArrayResource schemaResource = new ByteArrayResource(
"type Query { greeting: String }".getBytes(StandardCharsets.UTF_8));
return GraphQlSource.schemaResourceBuilder().schemaResources(schemaResource).build();
}
}
@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.SchemaResourceBuilder builder) {
this.applied = true;
}
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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 java.util.List;
import graphql.schema.DataFetcher;
import reactor.core.publisher.Flux;
import org.springframework.lang.Nullable;
/**
* Test utility class holding {@link DataFetcher} implementations.
*
* @author Brian Clozel
*/
public final class GraphQlTestDataFetchers {
private static List<Book> books = Arrays.asList(new Book("book-1", "GraphQL for beginners", 100, "John GraphQL"),
new Book("book-2", "Harry Potter and the Philosopher's Stone", 223, "Joanne Rowling"),
new Book("book-3", "Moby Dick", 635, "Moby Dick"), new Book("book-3", "Moby Dick", 635, "Moby Dick"));
private GraphQlTestDataFetchers() {
}
public static DataFetcher<Book> getBookByIdDataFetcher() {
return (environment) -> getBookById(environment.getArgument("id"));
}
public static DataFetcher<Flux<Book>> getBooksOnSaleDataFetcher() {
return (environment) -> getBooksOnSale(environment.getArgument("minPages"));
}
@Nullable
public static Book getBookById(String id) {
return books.stream().filter((book) -> book.getId().equals(id)).findFirst().orElse(null);
}
public static Flux<Book> getBooksOnSale(int minPages) {
return Flux.fromIterable(books).filter((book) -> book.getPageCount() >= minPages);
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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 com.querydsl.core.types.Path;
import com.querydsl.core.types.PathMetadata;
import com.querydsl.core.types.PathMetadataFactory;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringPath;
/**
* QBook is a Querydsl query type for Book. This class is usually generated by the
* Querydsl annotation processor.
*/
public class QBook extends EntityPathBase<Book> {
private static final long serialVersionUID = -1932588188L;
public static final QBook book = new QBook("book");
public final StringPath author = createString("author");
public final StringPath id = createString("id");
public final StringPath name = createString("name");
public final NumberPath<Integer> pageCount = createNumber("pageCount", Integer.class);
public QBook(String variable) {
super(Book.class, PathMetadataFactory.forVariable(variable));
}
public QBook(Path<? extends Book> path) {
super(path.getType(), path.getMetadata());
}
public QBook(PathMetadata metadata) {
super(Book.class, metadata);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2022 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.data;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlQueryByExampleAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlQueryByExampleAutoConfigurationTests {
private static final Book book = new Book("42", "Test title", 42, "Test Author");
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(GraphQlAutoConfiguration.class, GraphQlQueryByExampleAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryByExampleRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
ExecutionGraphQlServiceTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(Optional.of(book));
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends CrudRepository<Book, Long>, QueryByExampleExecutor<Book> {
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2022 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.data;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlQuerydslAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlQuerydslAutoConfigurationTests {
private static final Book book = new Book("42", "Test title", 42, "Test Author");
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(GraphQlAutoConfiguration.class, GraphQlQuerydslAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryDslRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
GraphQlTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(Optional.of(book));
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book> {
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2022 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.data;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlReactiveQueryByExampleAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlReactiveQueryByExampleAutoConfigurationTests {
private static final Mono<Book> bookPublisher = Mono.just(new Book("42", "Test title", 42, "Test Author"));
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class,
GraphQlReactiveQueryByExampleAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryByExampleRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
GraphQlTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(bookPublisher);
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends ReactiveCrudRepository<Book, Long>, ReactiveQueryByExampleExecutor<Book> {
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2022 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.data;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlReactiveQuerydslAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlReactiveQuerydslAutoConfigurationTests {
private static final Mono<Book> bookPublisher = Mono.just(new Book("42", "Test title", 42, "Test Author"));
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlAutoConfiguration.class,
GraphQlReactiveQuerydslAutoConfiguration.class))
.withUserConfiguration(MockRepositoryConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void shouldRegisterDataFetcherForQueryDslRepositories() {
this.contextRunner.run((context) -> {
ExecutionGraphQlService graphQlService = context.getBean(ExecutionGraphQlService.class);
GraphQlTester graphQlTester = ExecutionGraphQlServiceTester.create(graphQlService);
graphQlTester.document("{ bookById(id: 1) {name}}").execute().path("bookById.name").entity(String.class)
.isEqualTo("Test title");
});
}
@Configuration(proxyBeanMethods = false)
static class MockRepositoryConfig {
@Bean
MockRepository mockRepository() {
MockRepository mockRepository = mock(MockRepository.class);
given(mockRepository.findBy(any(), any())).willReturn(bookPublisher);
return mockRepository;
}
}
@GraphQlRepository
interface MockRepository extends ReactiveCrudRepository<Book, Long>, ReactiveQuerydslPredicateExecutor<Book> {
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2012-2022 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.reactive;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webflux.GraphQlHttpHandler;
import org.springframework.graphql.server.webflux.GraphQlWebSocketHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
/**
* Tests for {@link GraphQlWebFluxAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlWebFluxAutoConfigurationTests {
private static final String BASE_URL = "https://spring.example.org/";
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlWebFluxAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.graphiql.enabled=true",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.cors.allowed-origins=https://example.com",
"spring.graphql.cors.allowed-methods=POST", "spring.graphql.cors.allow-credentials=true");
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlHttpHandler.class)
.hasSingleBean(WebGraphQlHandler.class).doesNotHaveBean(GraphQlWebSocketHandler.class));
}
@Test
void simpleQueryShouldWork() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectHeader().contentType("application/json").expectBody().jsonPath("data.bookById.name")
.isEqualTo("GraphQL for beginners");
});
}
@Test
void httpGetQueryShouldBeSupported() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
client.get().uri("/graphql?query={query}", "{ \"query\": \"" + query + "\"}").exchange().expectStatus()
.isEqualTo(HttpStatus.METHOD_NOT_ALLOWED).expectHeader().valueEquals("Allow", "POST");
});
}
@Test
void shouldRejectMissingQuery() {
testWithWebClient(
(client) -> client.post().uri("/graphql").bodyValue("{}").exchange().expectStatus().isBadRequest());
}
@Test
void shouldRejectQueryWithInvalidJson() {
testWithWebClient(
(client) -> client.post().uri("/graphql").bodyValue(":)").exchange().expectStatus().isBadRequest());
}
@Test
void shouldConfigureWebInterceptors() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectHeader().valueEquals("X-Custom-Header", "42");
});
}
@Test
void shouldExposeSchemaEndpoint() {
testWithWebClient((client) -> client.get().uri("/graphql/schema").accept(MediaType.ALL).exchange()
.expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_PLAIN).expectBody(String.class)
.value(containsString("type Book")));
}
@Test
void shouldExposeGraphiqlEndpoint() {
testWithWebClient((client) -> {
client.get().uri("/graphiql").exchange().expectStatus().is3xxRedirection().expectHeader()
.location("https://spring.example.org/graphiql?path=/graphql");
client.get().uri("/graphiql?path=/graphql").accept(MediaType.ALL).exchange().expectStatus().isOk()
.expectHeader().contentType(MediaType.TEXT_HTML);
});
}
@Test
void shouldSupportCors() {
testWithWebClient((client) -> {
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
+ " author" + " }" + "}";
client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(HttpHeaders.ORIGIN, "https://example.com").exchange().expectStatus().isOk().expectHeader()
.valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://example.com").expectHeader()
.valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
});
}
@Test
void shouldConfigureWebSocketBeans() {
this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws")
.run((context) -> assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class));
}
@Test
void routerFunctionShouldHaveOrderZero() throws Exception {
this.contextRunner.withUserConfiguration(CustomRouterFunctions.class).run((context) -> {
Map<String, ?> beans = context.getBeansOfType(RouterFunction.class);
Object[] ordered = context.getBeanProvider(RouterFunction.class).orderedStream().toArray();
assertThat(beans.get("before")).isSameAs(ordered[0]);
assertThat(beans.get("graphQlRouterFunction")).isSameAs(ordered[1]);
assertThat(beans.get("after")).isSameAs(ordered[2]);
});
}
private void testWithWebClient(Consumer<WebTestClient> consumer) {
this.contextRunner.run((context) -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient()
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
}).baseUrl(BASE_URL).build();
consumer.accept(client);
});
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher() {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
@Bean
WebGraphQlInterceptor customWebGraphQlInterceptor() {
return (webInput, interceptorChain) -> interceptorChain.next(webInput)
.doOnNext((output) -> output.getResponseHeaders().add("X-Custom-Header", "42"));
}
}
@Configuration
static class CustomRouterFunctions {
@Bean
@Order(-1)
RouterFunction<?> before() {
return (r) -> null;
}
@Bean
@Order(1)
RouterFunction<?> after() {
return (r) -> null;
}
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2012-2022 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.rsocket;
import java.net.URI;
import java.time.Duration;
import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.embedded.netty.NettyRouteProvider;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.client.RSocketGraphQlClient;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlRSocketAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlRSocketAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class)
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlRSocketHandler.class)
.hasSingleBean(GraphQlRSocketController.class));
}
@Test
void simpleQueryShouldWorkWithTcpServer() {
testWithRSocketTcp(this::assertThatSimpleQueryWorks);
}
@Test
void simpleQueryShouldWorkWithWebSocketServer() {
testWithRSocketWebSocket(this::assertThatSimpleQueryWorks);
}
private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) {
String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }";
String bookName = client.document(document).retrieve("bookById.name").toEntity(String.class)
.block(Duration.ofSeconds(5));
assertThat(bookName).isEqualTo("GraphQL for beginners");
}
private void testWithRSocketTcp(Consumer<RSocketGraphQlClient> consumer) {
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class).withPropertyValues(
"spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
contextRunner.withInitializer(new RSocketPortInfoApplicationContextInitializer())
.withPropertyValues("spring.rsocket.server.port=0").run((context) -> {
String serverPort = context.getEnvironment().getProperty("local.rsocket.server.port");
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.tcp("localhost", Integer.parseInt(serverPort)).route("graphql").build();
consumer.accept(client);
});
}
private void testWithRSocketWebSocket(Consumer<RSocketGraphQlClient> consumer) {
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
AnnotationConfigReactiveWebServerApplicationContext::new).withConfiguration(
AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
ErrorWebFluxAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withUserConfiguration(DataFetchersConfiguration.class, NettyServerConfiguration.class)
.withPropertyValues("spring.main.web-application-type=reactive", "server.port=0",
"spring.graphql.rsocket.mapping=graphql", "spring.rsocket.server.transport=websocket",
"spring.rsocket.server.mapping-path=/rsocket");
contextRunner.run((context) -> {
String serverPort = context.getEnvironment().getProperty("local.server.port");
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.webSocket(URI.create("ws://localhost:" + serverPort + "/rsocket")).route("graphql").build();
consumer.accept(client);
});
}
@Configuration(proxyBeanMethods = false)
static class NettyServerConfiguration {
@Bean
NettyReactiveWebServerFactory serverFactory(NettyRouteProvider routeProvider) {
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0);
serverFactory.addRouteProviders(routeProvider);
return serverFactory;
}
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher() {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2012-2022 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.rsocket;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.client.RSocketGraphQlClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RSocketGraphQlClientAutoConfiguration}.
*
* @author Brian Clozel
*/
class RSocketGraphQlClientAutoConfigurationTests {
private static final RSocketGraphQlClient.Builder<?> builderInstance = RSocketGraphQlClient.builder();
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RSocketStrategiesAutoConfiguration.class,
RSocketRequesterAutoConfiguration.class, RSocketGraphQlClientAutoConfiguration.class));
@Test
void shouldCreateBuilder() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(RSocketGraphQlClient.Builder.class));
}
@Test
void shouldGetPrototypeScopedBean() {
this.contextRunner.run((context) -> {
RSocketGraphQlClient.Builder<?> first = context.getBean(RSocketGraphQlClient.Builder.class);
RSocketGraphQlClient.Builder<?> second = context.getBean(RSocketGraphQlClient.Builder.class);
assertThat(first).isNotEqualTo(second);
});
}
@Test
void shouldNotCreateBuilderIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(CustomRSocketGraphQlClientBuilder.class).run((context) -> {
RSocketGraphQlClient.Builder<?> builder = context.getBean(RSocketGraphQlClient.Builder.class);
assertThat(builder).isEqualTo(builderInstance);
});
}
@Configuration(proxyBeanMethods = false)
static class CustomRSocketGraphQlClientBuilder {
@Bean
RSocketGraphQlClient.Builder<?> myRSocketGraphQlClientBuilder() {
return builderInstance;
}
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2020-2022 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.security;
import java.util.Collections;
import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.graphql.reactive.GraphQlWebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Tests for {@link GraphQlWebFluxSecurityAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlWebFluxSecurityAutoConfigurationTests {
private static final String BASE_URL = "https://spring.example.org/graphql";
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlWebFluxAutoConfiguration.class, GraphQlWebFluxSecurityAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues("spring.main.web-application-type=reactive");
@Test
void contributesExceptionResolver() {
this.contextRunner.run(
(context) -> assertThat(context).hasSingleBean(ReactiveSecurityDataFetcherExceptionResolver.class));
}
@Test
void anonymousUserShouldBeUnauthorized() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
client.post().uri("").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectBody().jsonPath("data.bookById.name").doesNotExist()
.jsonPath("errors[0].extensions.classification").isEqualTo(ErrorType.UNAUTHORIZED.toString());
});
}
@Test
void authenticatedUserShouldGetData() {
testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
client.post().uri("").headers((headers) -> headers.setBasicAuth("rob", "rob"))
.bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk().expectBody()
.jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners")
.jsonPath("errors[0].extensions.classification").doesNotExist();
});
}
private void testWithWebClient(Consumer<WebTestClient> consumer) {
this.contextRunner.run((context) -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient()
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
}).baseUrl(BASE_URL).build();
consumer.accept(client);
});
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher(BookService bookService) {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
(env) -> bookService.getBookdById(env.getArgument("id"))));
}
@Bean
BookService bookService() {
return new BookService();
}
}
static class BookService {
@PreAuthorize("hasRole('USER')")
@Nullable
Mono<Book> getBookdById(String id) {
return Mono.justOrEmpty(GraphQlTestDataFetchers.getBookById(id));
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
static class SecurityConfig {
@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
return http.csrf((spec) -> spec.disable())
// Demonstrate that method security works
// Best practice to use both for defense in depth
.authorizeExchange((requests) -> requests.anyExchange().permitAll()).httpBasic(withDefaults())
.build();
}
@Bean
@SuppressWarnings("deprecation")
MapReactiveUserDetailsService userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
return new MapReactiveUserDetailsService(rob, admin);
}
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2020-2022 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.security;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.Book;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.SecurityContextThreadLocalAccessor;
import org.springframework.graphql.execution.SecurityDataFetcherExceptionResolver;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link GraphQlWebMvcSecurityAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlWebMvcSecurityAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class,
GraphQlWebMvcSecurityAutoConfiguration.class, SecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues("spring.main.web-application-type=servlet");
@Test
void contributesSecurityComponents() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(SecurityDataFetcherExceptionResolver.class);
assertThat(context).hasSingleBean(SecurityContextThreadLocalAccessor.class);
});
}
@Test
void anonymousUserShouldBeUnauthorized() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("data.bookById.name").doesNotExist()).andExpect(
jsonPath("errors[0].extensions.classification").value(ErrorType.UNAUTHORIZED.toString()));
});
}
@Test
void authenticatedUserShouldGetData() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
MvcResult result = mockMvc
.perform(post("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob"))).andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners"))
.andExpect(jsonPath("errors").doesNotExist());
});
}
private void testWith(MockMvcConsumer mockMvcConsumer) {
this.contextRunner.run((context) -> {
MediaType mediaType = MediaType.APPLICATION_JSON;
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context)
.defaultRequest(post("/graphql").contentType(mediaType).accept(mediaType)).apply(springSecurity())
.build();
mockMvcConsumer.accept(mockMvc);
});
}
private interface MockMvcConsumer {
void accept(MockMvc mockMvc) throws Exception;
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher(BookService bookService) {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
(env) -> bookService.getBookdById(env.getArgument("id"))));
}
@Bean
BookService bookService() {
return new BookService();
}
}
static class BookService {
@PreAuthorize("hasRole('USER')")
@Nullable
Book getBookdById(String id) {
return GraphQlTestDataFetchers.getBookById(id);
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@SuppressWarnings("deprecation")
static class SecurityConfig {
@Bean
DefaultSecurityFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
return http.csrf((c) -> c.disable())
// Demonstrate that method security works
// Best practice to use both for defense in depth
.authorizeRequests((requests) -> requests.anyRequest().permitAll()).httpBasic(withDefaults())
.build();
}
@Bean
InMemoryUserDetailsManager userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
UserDetails admin = userBuilder.username("admin").password("admin").roles("USER", "ADMIN").build();
return new InMemoryUserDetailsManager(rob, admin);
}
}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2012-2022 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.servlet;
import java.util.Map;
import graphql.schema.idl.TypeRuntimeWiring;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
import org.springframework.graphql.server.webmvc.GraphQlWebSocketHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.function.RouterFunction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for {@link GraphQlWebMvcAutoConfiguration}.
*
* @author Brian Clozel
*/
class GraphQlWebMvcAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues("spring.main.web-application-type=servlet", "spring.graphql.graphiql.enabled=true",
"spring.graphql.schema.printer.enabled=true",
"spring.graphql.cors.allowed-origins=https://example.com",
"spring.graphql.cors.allowed-methods=POST", "spring.graphql.cors.allow-credentials=true");
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlHttpHandler.class)
.hasSingleBean(WebGraphQlHandler.class).doesNotHaveBean(GraphQlWebSocketHandler.class));
}
@Test
void simpleQueryShouldWork() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL))
.andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners"));
});
}
@Test
void httpGetQueryShouldBeSupported() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
mockMvc.perform(get("/graphql?query={query}", "{\"query\": \"" + query + "\"}"))
.andExpect(status().isMethodNotAllowed()).andExpect(header().string("Allow", "POST"));
});
}
@Test
void shouldRejectMissingQuery() {
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content("{}")).andExpect(status().isBadRequest()));
}
@Test
void shouldRejectQueryWithInvalidJson() {
testWith((mockMvc) -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest()));
}
@Test
void shouldConfigureWebInterceptors() {
testWith((mockMvc) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(header().string("X-Custom-Header", "42"));
});
}
@Test
void shouldExposeSchemaEndpoint() {
testWith((mockMvc) -> mockMvc.perform(get("/graphql/schema")).andExpect(status().isOk())
.andExpect(content().contentType(MediaType.TEXT_PLAIN))
.andExpect(content().string(Matchers.containsString("type Book"))));
}
@Test
void shouldExposeGraphiqlEndpoint() {
testWith((mockMvc) -> {
mockMvc.perform(get("/graphiql")).andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("http://localhost/graphiql?path=/graphql"));
mockMvc.perform(get("/graphiql?path=/graphql")).andExpect(status().isOk())
.andExpect(content().contentType(MediaType.TEXT_HTML));
});
}
@Test
void shouldSupportCors() {
testWith((mockMvc) -> {
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
+ " author" + " }" + "}";
MvcResult result = mockMvc.perform(post("/graphql")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(HttpHeaders.ORIGIN, "https://example.com").content("{\"query\": \"" + query + "\"}"))
.andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://example.com"))
.andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"));
});
}
@Test
void shouldConfigureWebSocketBeans() {
this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws")
.run((context) -> assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class));
}
@Test
void routerFunctionShouldHaveOrderZero() throws Exception {
this.contextRunner.withUserConfiguration(CustomRouterFunctions.class).run((context) -> {
Map<String, ?> beans = context.getBeansOfType(RouterFunction.class);
Object[] ordered = context.getBeanProvider(RouterFunction.class).orderedStream().toArray();
assertThat(beans.get("before")).isSameAs(ordered[0]);
assertThat(beans.get("graphQlRouterFunction")).isSameAs(ordered[1]);
assertThat(beans.get("after")).isSameAs(ordered[2]);
});
}
private void testWith(MockMvcConsumer mockMvcConsumer) {
this.contextRunner.run((context) -> {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).defaultRequest(
post("/graphql").contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL))
.build();
mockMvcConsumer.accept(mockMvc);
});
}
private interface MockMvcConsumer {
void accept(MockMvc mockMvc) throws Exception;
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher() {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
@Bean
WebGraphQlInterceptor customWebGraphQlInterceptor() {
return (webInput, interceptorChain) -> interceptorChain.next(webInput)
.doOnNext((output) -> output.getResponseHeaders().add("X-Custom-Header", "42"));
}
}
@Configuration
static class CustomRouterFunctions {
@Bean
@Order(-1)
RouterFunction<?> before() {
return (r) -> null;
}
@Bean
@Order(1)
RouterFunction<?> after() {
return (r) -> null;
}
}
}

View File

@@ -0,0 +1,4 @@
type Query {
greeting(name: String! = "Spring"): String!
bookById(id: ID): Book
}

View File

@@ -0,0 +1,6 @@
type Book {
id: ID
name: String
pageCount: Int
author: String
}

View File

@@ -0,0 +1,4 @@
type Person {
id: ID
name: String
}