Create spring-boot-graphql module
This commit is contained in:
committed by
Phillip Webb
parent
837cf467e0
commit
978de86361
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
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, through schema files or infrastructure beans.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Conditional(DefaultGraphQlSchemaCondition.class)
|
||||
public @interface ConditionalOnGraphQlSchema {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
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;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
|
||||
/**
|
||||
* {@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 {@link GraphQlSourceBuilderCustomizer} beans</li>
|
||||
* <li>or a {@link GraphQlSource} bean</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()));
|
||||
}
|
||||
String[] graphQlSourceBeanNames = beanFactory.getBeanNamesForType(GraphQlSource.class, false, false);
|
||||
if (graphQlSourceBeanNames.length != 0) {
|
||||
match = true;
|
||||
messages.add(message.found("GraphQlSource").items(Arrays.asList(graphQlSourceBeanNames)));
|
||||
}
|
||||
else {
|
||||
messages.add((message.didNotFind("GraphQlSource").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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import graphql.execution.instrumentation.Instrumentation;
|
||||
import graphql.introspection.Introspection;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
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.task.TaskExecutionAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.graphql.ExecutionGraphQlService;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
|
||||
import org.springframework.graphql.data.pagination.CursorEncoder;
|
||||
import org.springframework.graphql.data.pagination.CursorStrategy;
|
||||
import org.springframework.graphql.data.pagination.EncodingCursorStrategy;
|
||||
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
|
||||
import org.springframework.graphql.data.query.SliceConnectionAdapter;
|
||||
import org.springframework.graphql.data.query.WindowConnectionAdapter;
|
||||
import org.springframework.graphql.execution.BatchLoaderRegistry;
|
||||
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
|
||||
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 4.0.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class })
|
||||
@ConditionalOnGraphQlSchema
|
||||
@EnableConfigurationProperties(GraphQlProperties.class)
|
||||
@ImportRuntimeHints(GraphQlAutoConfiguration.GraphQlResourcesRuntimeHints.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();
|
||||
List<Resource> schemaResources = new ArrayList<>();
|
||||
schemaResources.addAll(resolveSchemaResources(resourcePatternResolver, schemaLocations,
|
||||
properties.getSchema().getFileExtensions()));
|
||||
schemaResources.addAll(Arrays.asList(properties.getSchema().getAdditionalFiles()));
|
||||
|
||||
GraphQlSource.SchemaResourceBuilder builder = GraphQlSource.schemaResourceBuilder()
|
||||
.schemaResources(schemaResources.toArray(new Resource[0]))
|
||||
.exceptionResolvers(exceptionResolvers.orderedStream().toList())
|
||||
.subscriptionExceptionResolvers(subscriptionExceptionResolvers.orderedStream().toList())
|
||||
.instrumentation(instrumentations.orderedStream().toList());
|
||||
if (properties.getSchema().getInspection().isEnabled()) {
|
||||
builder.inspectSchemaMappings(logger::info);
|
||||
}
|
||||
if (!properties.getSchema().getIntrospection().isEnabled()) {
|
||||
Introspection.enabledJvmWide(false);
|
||||
}
|
||||
builder.configureTypeDefinitions(new ConnectionTypeDefinitionConfigurer());
|
||||
wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring);
|
||||
sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
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) {
|
||||
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(
|
||||
@Qualifier(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) ObjectProvider<Executor> executorProvider,
|
||||
ObjectProvider<HandlerMethodArgumentResolver> argumentResolvers) {
|
||||
AnnotatedControllerConfigurer controllerConfigurer = new AnnotatedControllerConfigurer();
|
||||
controllerConfigurer
|
||||
.addFormatterRegistrar((registry) -> ApplicationConversionService.addBeans(registry, this.beanFactory));
|
||||
executorProvider.ifAvailable(controllerConfigurer::setExecutor);
|
||||
argumentResolvers.orderedStream().forEach(controllerConfigurer::addCustomArgumentResolver);
|
||||
return controllerConfigurer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
DataFetcherExceptionResolver annotatedControllerConfigurerDataFetcherExceptionResolver(
|
||||
AnnotatedControllerConfigurer annotatedControllerConfigurer) {
|
||||
return annotatedControllerConfigurer.getExceptionResolver();
|
||||
}
|
||||
|
||||
@ConditionalOnClass(ScrollPosition.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GraphQlDataAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
EncodingCursorStrategy<ScrollPosition> cursorStrategy() {
|
||||
return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("unchecked")
|
||||
GraphQlSourceBuilderCustomizer cursorStrategyCustomizer(CursorStrategy<?> cursorStrategy) {
|
||||
if (cursorStrategy.supports(ScrollPosition.class)) {
|
||||
CursorStrategy<ScrollPosition> scrollCursorStrategy = (CursorStrategy<ScrollPosition>) cursorStrategy;
|
||||
ConnectionFieldTypeVisitor connectionFieldTypeVisitor = ConnectionFieldTypeVisitor
|
||||
.create(List.of(new WindowConnectionAdapter(scrollCursorStrategy),
|
||||
new SliceConnectionAdapter(scrollCursorStrategy)));
|
||||
return (builder) -> builder.typeVisitors(List.of(connectionFieldTypeVisitor));
|
||||
}
|
||||
return (builder) -> {
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class GraphQlResourcesRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
hints.resources().registerPattern("graphql/**/*.graphqls").registerPattern("graphql/**/*.gqls");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
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 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.graphql.cors")
|
||||
public class GraphQlCorsProperties {
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
|
||||
/**
|
||||
* List of HTTP methods to allow. '*' allows all methods. When not set, defaults to
|
||||
* GET.
|
||||
*/
|
||||
private List<String> allowedMethods = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* List of HTTP headers to allow in a request. '*' allows all headers.
|
||||
*/
|
||||
private List<String> allowedHeaders = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties Properties} for Spring GraphQL.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.graphql")
|
||||
public class GraphQlProperties {
|
||||
|
||||
private final Http http = new Http();
|
||||
|
||||
private final Graphiql graphiql = new Graphiql();
|
||||
|
||||
private final Rsocket rsocket = new Rsocket();
|
||||
|
||||
private final Schema schema = new Schema();
|
||||
|
||||
private final DeprecatedSse sse = new DeprecatedSse(this.http.getSse());
|
||||
|
||||
private final Websocket websocket = new Websocket();
|
||||
|
||||
public Http getHttp() {
|
||||
return this.http;
|
||||
}
|
||||
|
||||
public Graphiql getGraphiql() {
|
||||
return this.graphiql;
|
||||
}
|
||||
|
||||
@DeprecatedConfigurationProperty(replacement = "spring.graphql.http.path", since = "3.5.0")
|
||||
@Deprecated(since = "3.5.0", forRemoval = true)
|
||||
public String getPath() {
|
||||
return getHttp().getPath();
|
||||
}
|
||||
|
||||
@Deprecated(since = "3.5.0", forRemoval = true)
|
||||
public void setPath(String path) {
|
||||
getHttp().setPath(path);
|
||||
}
|
||||
|
||||
public Schema getSchema() {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
public Websocket getWebsocket() {
|
||||
return this.websocket;
|
||||
}
|
||||
|
||||
public Rsocket getRsocket() {
|
||||
return this.rsocket;
|
||||
}
|
||||
|
||||
public DeprecatedSse getSse() {
|
||||
return this.sse;
|
||||
}
|
||||
|
||||
public static class Http {
|
||||
|
||||
/**
|
||||
* Path at which to expose a GraphQL request HTTP endpoint.
|
||||
*/
|
||||
private String path = "/graphql";
|
||||
|
||||
private final Sse sse = new Sse();
|
||||
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public Sse getSse() {
|
||||
return this.sse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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" };
|
||||
|
||||
/**
|
||||
* Locations of additional, individual schema files to parse.
|
||||
*/
|
||||
private Resource[] additionalFiles = new Resource[0];
|
||||
|
||||
private final Inspection inspection = new Inspection();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Resource[] getAdditionalFiles() {
|
||||
return this.additionalFiles;
|
||||
}
|
||||
|
||||
public void setAdditionalFiles(Resource[] additionalFiles) {
|
||||
this.additionalFiles = additionalFiles;
|
||||
}
|
||||
|
||||
private String[] appendSlashIfNecessary(String[] locations) {
|
||||
return Arrays.stream(locations)
|
||||
.map((location) -> location.endsWith("/") ? location : location + "/")
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
public Inspection getInspection() {
|
||||
return this.inspection;
|
||||
}
|
||||
|
||||
public Introspection getIntrospection() {
|
||||
return this.introspection;
|
||||
}
|
||||
|
||||
public Printer getPrinter() {
|
||||
return this.printer;
|
||||
}
|
||||
|
||||
public static class Inspection {
|
||||
|
||||
/**
|
||||
* Whether schema should be compared to the application to detect missing
|
||||
* mappings.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.http.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);
|
||||
|
||||
/**
|
||||
* Maximum idle period before a server keep-alive ping is sent to client.
|
||||
*/
|
||||
private Duration keepAlive;
|
||||
|
||||
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 Duration getKeepAlive() {
|
||||
return this.keepAlive;
|
||||
}
|
||||
|
||||
public void setKeepAlive(Duration keepAlive) {
|
||||
this.keepAlive = keepAlive;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Sse {
|
||||
|
||||
/**
|
||||
* How frequently keep-alive messages should be sent.
|
||||
*/
|
||||
private Duration keepAlive;
|
||||
|
||||
/**
|
||||
* Time required for concurrent handling to complete.
|
||||
*/
|
||||
private Duration timeout;
|
||||
|
||||
public Duration getKeepAlive() {
|
||||
return this.keepAlive;
|
||||
}
|
||||
|
||||
public void setKeepAlive(Duration keepAlive) {
|
||||
this.keepAlive = keepAlive;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Deprecated(since = "3.5.1", forRemoval = true)
|
||||
public static final class DeprecatedSse {
|
||||
|
||||
private final Sse sse;
|
||||
|
||||
private DeprecatedSse(Sse sse) {
|
||||
this.sse = sse;
|
||||
}
|
||||
|
||||
@DeprecatedConfigurationProperty(replacement = "spring.graphql.http.sse.timeout", since = "3.5.0")
|
||||
@Deprecated(since = "3.5.0", forRemoval = true)
|
||||
public Duration getTimeout() {
|
||||
return this.sse.getTimeout();
|
||||
}
|
||||
|
||||
@Deprecated(since = "3.5.0", forRemoval = true)
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.sse.setTimeout(timeout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
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 4.0.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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import java.util.Collections;
|
||||
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.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlSourceBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
|
||||
/**
|
||||
* {@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 4.0.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) {
|
||||
RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher
|
||||
.autoRegistrationConfigurer(executors.orderedStream().toList(), Collections.emptyList());
|
||||
return (builder) -> builder.configureRuntimeWiring(configurer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.querydsl.core.Query;
|
||||
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.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlSourceBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||
import org.springframework.graphql.data.query.QuerydslDataFetcher;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
|
||||
/**
|
||||
* {@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 4.0.0
|
||||
* @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List)
|
||||
*/
|
||||
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
|
||||
@ConditionalOnClass({ GraphQL.class, Query.class, QuerydslDataFetcher.class, QuerydslPredicateExecutor.class })
|
||||
@ConditionalOnBean(GraphQlSource.class)
|
||||
public class GraphQlQuerydslAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public GraphQlSourceBuilderCustomizer querydslRegistrar(ObjectProvider<QuerydslPredicateExecutor<?>> executors) {
|
||||
RuntimeWiringConfigurer configurer = QuerydslDataFetcher
|
||||
.autoRegistrationConfigurer(executors.orderedStream().toList(), Collections.emptyList());
|
||||
return (builder) -> builder.configureRuntimeWiring(configurer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import java.util.Collections;
|
||||
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.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlSourceBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
|
||||
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
|
||||
/**
|
||||
* {@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 4.0.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) {
|
||||
RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher
|
||||
.autoRegistrationConfigurer(Collections.emptyList(), reactiveExecutors.orderedStream().toList());
|
||||
return (builder) -> builder.configureRuntimeWiring(configurer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.querydsl.core.Query;
|
||||
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.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlSourceBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
|
||||
import org.springframework.graphql.data.query.QuerydslDataFetcher;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
|
||||
/**
|
||||
* {@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 4.0.0
|
||||
* @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List)
|
||||
*/
|
||||
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
|
||||
@ConditionalOnClass({ GraphQL.class, Query.class, QuerydslDataFetcher.class, ReactiveQuerydslPredicateExecutor.class })
|
||||
@ConditionalOnBean(GraphQlSource.class)
|
||||
public class GraphQlReactiveQuerydslAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public GraphQlSourceBuilderCustomizer reactiveQuerydslRegistrar(
|
||||
ObjectProvider<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
|
||||
RuntimeWiringConfigurer configurer = QuerydslDataFetcher.autoRegistrationConfigurer(Collections.emptyList(),
|
||||
reactiveExecutors.orderedStream().toList());
|
||||
return (builder) -> builder.configureRuntimeWiring(configurer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.reactive;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
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.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
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.GraphQlRequestPredicates;
|
||||
import org.springframework.graphql.server.webflux.GraphQlSseHandler;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||
* WebFlux.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
|
||||
@ConditionalOnBean(ExecutionGraphQlService.class)
|
||||
@EnableConfigurationProperties(GraphQlCorsProperties.class)
|
||||
@ImportRuntimeHints(GraphQlWebFluxAutoConfiguration.GraphiQlResourceHints.class)
|
||||
public class GraphQlWebFluxAutoConfiguration {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||
return new GraphQlHttpHandler(webGraphQlHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
|
||||
return new GraphQlSseHandler(webGraphQlHandler, properties.getHttp().getSse().getTimeout(),
|
||||
properties.getHttp().getSse().getKeepAlive());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
|
||||
ObjectProvider<WebGraphQlInterceptor> interceptors) {
|
||||
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
|
||||
GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
|
||||
GraphQlProperties properties) {
|
||||
String path = properties.getHttp().getPath();
|
||||
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
|
||||
RouterFunctions.Builder builder = RouterFunctions.route();
|
||||
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
|
||||
builder.route(GraphQlRequestPredicates.graphQlSse(path), sseHandler::handleRequest);
|
||||
builder.POST(path, this::unsupportedMediaType);
|
||||
builder.GET(path, this::onlyAllowPost);
|
||||
if (properties.getGraphiql().isEnabled()) {
|
||||
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
|
||||
builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest);
|
||||
}
|
||||
GraphQlSource graphQlSource = graphQlSourceProvider.getIfAvailable();
|
||||
if (properties.getSchema().getPrinter().isEnabled() && graphQlSource != null) {
|
||||
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
||||
builder.GET(path + "/schema", schemaHandler::handleRequest);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> unsupportedMediaType(ServerRequest request) {
|
||||
return ServerResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).headers(this::acceptJson).build();
|
||||
}
|
||||
|
||||
private void acceptJson(HttpHeaders headers) {
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
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.getHttp().getPath()).combine(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty("spring.graphql.websocket.path")
|
||||
public static class WebSocketConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
|
||||
GraphQlProperties properties, ServerCodecConfigurer configurer) {
|
||||
return new GraphQlWebSocketHandler(webGraphQlHandler, configurer,
|
||||
properties.getWebsocket().getConnectionInitTimeout(), properties.getWebsocket().getKeepAlive());
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class GraphiQlResourceHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
hints.resources().registerPattern("graphiql/index.html");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.reactive;
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.rsocket;
|
||||
|
||||
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.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
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.messaging.rsocket.annotation.support.RSocketMessageHandler;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||
* RSocket.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = GraphQlAutoConfiguration.class,
|
||||
afterName = "org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration")
|
||||
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class })
|
||||
@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class })
|
||||
@ConditionalOnProperty("spring.graphql.rsocket.mapping")
|
||||
public class GraphQlRSocketAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@SuppressWarnings({ "removal", "deprecation" })
|
||||
public GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService,
|
||||
ObjectProvider<RSocketGraphQlInterceptor> interceptors, ObjectMapper objectMapper) {
|
||||
return new GraphQlRSocketHandler(graphQlService, interceptors.orderedStream().toList(),
|
||||
new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlRSocketController graphQlRSocketController(GraphQlRSocketHandler handler) {
|
||||
return new GraphQlRSocketController(handler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.rsocket;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import io.rsocket.RSocket;
|
||||
import io.rsocket.transport.netty.client.TcpClientTransport;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
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.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 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(afterName = "org.springframework.boot.rsocket.autoconfigure.RSocketRequesterAutoConfiguration")
|
||||
@ConditionalOnClass({ GraphQL.class, RSocketGraphQlClient.class, RSocketRequester.class, RSocket.class,
|
||||
TcpClientTransport.class })
|
||||
public class RSocketGraphQlClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@ConditionalOnMissingBean
|
||||
public RSocketGraphQlClient.Builder<?> rsocketGraphQlClientBuilder(
|
||||
RSocketRequester.Builder rsocketRequesterBuilder) {
|
||||
return RSocketGraphQlClient.builder(rsocketRequesterBuilder.dataMimeType(MimeTypeUtils.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.rsocket;
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.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.graphql.autoconfigure.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 4.0.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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.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.graphql.autoconfigure.servlet.GraphQlWebMvcAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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 4.0.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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.security;
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.servlet;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import jakarta.websocket.server.ServerContainer;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
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.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlCorsProperties;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlProperties;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
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.webmvc.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlRequestPredicates;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlSseHandler;
|
||||
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.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 4.0.0
|
||||
*/
|
||||
@AutoConfiguration(after = GraphQlAutoConfiguration.class)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
@ConditionalOnClass({ GraphQL.class, GraphQlHttpHandler.class })
|
||||
@ConditionalOnBean(ExecutionGraphQlService.class)
|
||||
@EnableConfigurationProperties(GraphQlCorsProperties.class)
|
||||
@ImportRuntimeHints(GraphQlWebMvcAutoConfiguration.GraphiQlResourceHints.class)
|
||||
public class GraphQlWebMvcAutoConfiguration {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class);
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
|
||||
return new GraphQlHttpHandler(webGraphQlHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
|
||||
return new GraphQlSseHandler(webGraphQlHandler, properties.getHttp().getSse().getTimeout(),
|
||||
properties.getHttp().getSse().getKeepAlive());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
|
||||
ObjectProvider<WebGraphQlInterceptor> interceptors) {
|
||||
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
|
||||
GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
|
||||
GraphQlProperties properties) {
|
||||
String path = properties.getHttp().getPath();
|
||||
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
|
||||
RouterFunctions.Builder builder = RouterFunctions.route();
|
||||
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
|
||||
builder.route(GraphQlRequestPredicates.graphQlSse(path), sseHandler::handleRequest);
|
||||
builder.POST(path, this::unsupportedMediaType);
|
||||
builder.GET(path, this::onlyAllowPost);
|
||||
if (properties.getGraphiql().isEnabled()) {
|
||||
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
|
||||
builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);
|
||||
}
|
||||
GraphQlSource graphQlSource = graphQlSourceProvider.getIfAvailable();
|
||||
if (properties.getSchema().getPrinter().isEnabled() && graphQlSource != null) {
|
||||
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
|
||||
builder.GET(path + "/schema", schemaHandler::handleRequest);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private ServerResponse unsupportedMediaType(ServerRequest request) {
|
||||
return ServerResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).headers(this::acceptJson).build();
|
||||
}
|
||||
|
||||
private void acceptJson(HttpHeaders headers) {
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
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.getHttp().getPath()).combine(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ HttpMessageConverters.class, ServerContainer.class, WebSocketHandler.class })
|
||||
@ConditionalOnProperty("spring.graphql.websocket.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(), properties.getWebsocket().getKeepAlive());
|
||||
}
|
||||
|
||||
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.initWebSocketHttpRequestHandler(new DefaultHandshakeHandler())));
|
||||
mapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
|
||||
return mapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class GraphiQlResourceHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
hints.resources().registerPattern("graphiql/index.html");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.servlet;
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"groups": [],
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.graphql.schema.file-extensions",
|
||||
"defaultValue": ".graphqls,.gqls"
|
||||
},
|
||||
{
|
||||
"name": "spring.graphql.schema.locations",
|
||||
"defaultValue": "classpath:graphql/**/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.data.GraphQlQueryByExampleAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.data.GraphQlQuerydslAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.data.GraphQlReactiveQueryByExampleAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.data.GraphQlReactiveQuerydslAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.reactive.GraphQlWebFluxAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.rsocket.GraphQlRSocketAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.rsocket.RSocketGraphQlClientAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.security.GraphQlWebFluxSecurityAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.security.GraphQlWebMvcSecurityAutoConfiguration
|
||||
org.springframework.boot.graphql.autoconfigure.servlet.GraphQlWebMvcAutoConfiguration
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
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.boot.testsupport.classpath.resources.WithResource;
|
||||
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
|
||||
@WithResource(name = "graphql/one.graphqls")
|
||||
@WithResource(name = "graphql/two.graphqls")
|
||||
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 myBuilderCustomizer");
|
||||
});
|
||||
}
|
||||
|
||||
@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 myBuilderCustomizer() {
|
||||
return (builder) -> {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import graphql.execution.instrumentation.ChainedInstrumentation;
|
||||
import graphql.execution.instrumentation.Instrumentation;
|
||||
import graphql.introspection.Introspection;
|
||||
import graphql.schema.GraphQLObjectType;
|
||||
import graphql.schema.GraphQLOutputType;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration.GraphQlResourcesRuntimeHints;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
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.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||
import org.springframework.graphql.data.pagination.EncodingCursorStrategy;
|
||||
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}.
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
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)
|
||||
.hasSingleBean(BatchLoaderRegistry.class)
|
||||
.hasSingleBean(ExecutionGraphQlService.class)
|
||||
.hasSingleBean(AnnotatedControllerConfigurer.class)
|
||||
.hasSingleBean(EncodingCursorStrategy.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
|
||||
@WithResource(name = "graphql/types/person.custom", content = """
|
||||
type Person {
|
||||
id: ID
|
||||
name: String
|
||||
}
|
||||
""")
|
||||
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
|
||||
@WithResource(name = "graphql/types/person.custom", content = """
|
||||
type Person {
|
||||
id: ID
|
||||
name: String
|
||||
}
|
||||
""")
|
||||
void shouldConfigureAdditionalSchemaFiles() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.graphql.schema.additional-files=classpath:graphql/types/person.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 shouldUseCustomGraphQlSource() {
|
||||
this.contextRunner.withUserConfiguration(CustomGraphQlSourceConfiguration.class).run((context) -> {
|
||||
assertThat(context).getBeanNames(GraphQlSource.class).containsOnly("customGraphQlSource");
|
||||
assertThat(context).hasSingleBean(GraphQlProperties.class)
|
||||
.hasSingleBean(BatchLoaderRegistry.class)
|
||||
.hasSingleBean(ExecutionGraphQlService.class)
|
||||
.hasSingleBean(AnnotatedControllerConfigurer.class)
|
||||
.hasSingleBean(EncodingCursorStrategy.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");
|
||||
assertThat(exceptionHandler).extracting("resolvers")
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.hasSize(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@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 schemaInspectionShouldBeEnabledByDefault(CapturedOutput output) {
|
||||
this.contextRunner.run((context) -> assertThat(output).contains("GraphQL schema inspection"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldIntrospectionShouldBeEnabledByDefault() {
|
||||
this.contextRunner.run((context) -> assertThat(Introspection.isEnabledJvmWide()).isTrue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDisableFieldIntrospection() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.schema.introspection.enabled:false")
|
||||
.run((context) -> assertThat(Introspection.isEnabledJvmWide()).isFalse());
|
||||
}
|
||||
|
||||
@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));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new GraphQlResourcesRuntimeHints().registerHints(hints, getClass().getClassLoader());
|
||||
assertThat(RuntimeHintsPredicates.resource().forResource("graphql/sample/schema.gqls")).accepts(hints);
|
||||
assertThat(RuntimeHintsPredicates.resource().forResource("graphql/other.graphqls")).accepts(hints);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContributeConnectionTypeDefinitionConfigurer() {
|
||||
this.contextRunner.withUserConfiguration(CustomGraphQlBuilderConfiguration.class).run((context) -> {
|
||||
GraphQlSource graphQlSource = context.getBean(GraphQlSource.class);
|
||||
GraphQLSchema schema = graphQlSource.schema();
|
||||
GraphQLOutputType bookConnection = schema.getQueryType().getField("books").getType();
|
||||
assertThat(bookConnection).isInstanceOf(GraphQLObjectType.class);
|
||||
assertThat((GraphQLObjectType) bookConnection)
|
||||
.satisfies((connection) -> assertThat(connection.getFieldDefinition("edges")).isNotNull());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenApplicationTaskExecutorIsDefinedThenAnnotatedControllerConfigurerShouldUseIt() {
|
||||
this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class))
|
||||
.run((context) -> {
|
||||
AnnotatedControllerConfigurer annotatedControllerConfigurer = context
|
||||
.getBean(AnnotatedControllerConfigurer.class);
|
||||
assertThat(annotatedControllerConfigurer).extracting("executor")
|
||||
.isSameAs(context.getBean("applicationTaskExecutor"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenCustomExecutorIsDefinedThenAnnotatedControllerConfigurerDoesNotUseIt() {
|
||||
this.contextRunner.withUserConfiguration(CustomExecutorConfiguration.class).run((context) -> {
|
||||
AnnotatedControllerConfigurer annotatedControllerConfigurer = context
|
||||
.getBean(AnnotatedControllerConfigurer.class);
|
||||
assertThat(annotatedControllerConfigurer).extracting("executor").isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenAHandlerMethodArgumentResolverIsDefinedThenAnnotatedControllerConfigurerShouldUseIt() {
|
||||
this.contextRunner.withUserConfiguration(CustomHandlerMethodArgumentResolverConfiguration.class)
|
||||
.run((context) -> assertThat(context.getBean(AnnotatedControllerConfigurer.class))
|
||||
.extracting("customArgumentResolvers")
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.hasSize(1));
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomExecutorConfiguration {
|
||||
|
||||
@Bean
|
||||
Executor customExecutor() {
|
||||
return mock(Executor.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomHandlerMethodArgumentResolverConfiguration {
|
||||
|
||||
@Bean
|
||||
HandlerMethodArgumentResolver customHandlerMethodArgumentResolver() {
|
||||
return mock(HandlerMethodArgumentResolver.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import graphql.schema.DataFetcher;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Test utility class holding {@link DataFetcher} implementations.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public final class GraphQlTestDataFetchers {
|
||||
|
||||
private static final 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"));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.Book;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
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
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
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=servlet");
|
||||
|
||||
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> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.Book;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
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.assertj.core.api.Assertions.assertThat;
|
||||
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
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
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=servlet");
|
||||
|
||||
@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");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackOffWithoutQueryDsl() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader("com.querydsl.core"))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean("querydslRegistrar")
|
||||
.doesNotHaveBean(GraphQlQuerydslAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@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> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.Book;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
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
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
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> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.data;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.Book;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
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.assertj.core.api.Assertions.assertThat;
|
||||
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
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBackOffWithoutQueryDsl() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader("com.querydsl.core"))
|
||||
.run((context) -> assertThat(context).doesNotHaveBean("querydslRegistrar")
|
||||
.doesNotHaveBean(GraphQlReactiveQuerydslAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@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> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.reactive;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import graphql.schema.idl.TypeRuntimeWiring;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlTestDataFetchers;
|
||||
import org.springframework.boot.http.codec.autoconfigure.CodecsAutoConfiguration;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
|
||||
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.GraphQlSseHandler;
|
||||
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.EntityExchangeResult;
|
||||
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
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
@Disabled("Waiting on compatible release")
|
||||
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(MediaType.APPLICATION_GRAPHQL_RESPONSE_VALUE)
|
||||
.expectBody()
|
||||
.jsonPath("data.bookById.name")
|
||||
.isEqualTo("GraphQL for beginners");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void SseSubscriptionShouldWork() {
|
||||
testWithWebClient((client) -> {
|
||||
String query = "{ booksOnSale(minPages: 50){ id name pageCount author } }";
|
||||
EntityExchangeResult<String> result = client.post()
|
||||
.uri("/graphql")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.bodyValue("{ \"query\": \"subscription TestSubscription " + query + "\"}")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM)
|
||||
.expectBody(String.class)
|
||||
.returnResult();
|
||||
assertThat(result.getResponseBody()).contains("event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-1\",\"name\":\"GraphQL for beginners\",\"pageCount\":100,\"author\":\"John GraphQL\"}}}",
|
||||
"event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-2\",\"name\":\"Harry Potter and the Philosopher's Stone\",\"pageCount\":223,\"author\":\"Joanne Rowling\"}}}",
|
||||
"event:complete");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedContentTypeShouldBeRejected() {
|
||||
testWithWebClient((client) -> {
|
||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
||||
client.post()
|
||||
.uri("/graphql")
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.bodyValue("{ \"query\": \"" + query + "\"}")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
.expectHeader()
|
||||
.valueEquals("Accept", "application/json");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpGetQueryShouldBeRejected() {
|
||||
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 shouldConfigureWebSocketProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.graphql.websocket.path=/ws",
|
||||
"spring.graphql.websocket.connection-init-timeout=120s", "spring.graphql.websocket.keep-alive=30s")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class);
|
||||
GraphQlWebSocketHandler graphQlWebSocketHandler = context.getBean(GraphQlWebSocketHandler.class);
|
||||
assertThat(graphQlWebSocketHandler).extracting("initTimeoutDuration")
|
||||
.isEqualTo(Duration.ofSeconds(120));
|
||||
assertThat(graphQlWebSocketHandler).extracting("keepAliveDuration").isEqualTo(Duration.ofSeconds(30));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureSseTimeout() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.http.sse.timeout=10s").run((context) -> {
|
||||
assertThat(context).hasSingleBean(GraphQlSseHandler.class);
|
||||
GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class);
|
||||
assertThat(handler).hasFieldOrPropertyWithValue("timeout", Duration.ofSeconds(10));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureSseKeepAlive() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.http.sse.keep-alive=5s").run((context) -> {
|
||||
assertThat(context).hasSingleBean(GraphQlSseHandler.class);
|
||||
GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class);
|
||||
assertThat(handler).hasFieldOrPropertyWithValue("keepAliveDuration", Duration.ofSeconds(5));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void routerFunctionShouldHaveOrderZero() {
|
||||
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]);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new GraphQlWebFluxAutoConfiguration.GraphiQlResourceHints().registerHints(hints, getClass().getClassLoader());
|
||||
assertThat(RuntimeHintsPredicates.resource().forResource("graphiql/index.html")).accepts(hints);
|
||||
}
|
||||
|
||||
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_GRAPHQL_RESPONSE));
|
||||
})
|
||||
.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()));
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Subscription")
|
||||
.dataFetcher("booksOnSale", GraphQlTestDataFetchers.getBooksOnSaleDataFetcher()));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.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.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlTestDataFetchers;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory;
|
||||
import org.springframework.boot.reactor.netty.NettyRouteProvider;
|
||||
import org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration;
|
||||
import org.springframework.boot.rsocket.autoconfigure.RSocketServerAutoConfiguration;
|
||||
import org.springframework.boot.rsocket.autoconfigure.RSocketStrategiesAutoConfiguration;
|
||||
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
|
||||
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
|
||||
import org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
|
||||
import org.springframework.boot.webflux.autoconfigure.error.ErrorWebFluxAutoConfiguration;
|
||||
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
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.rsocket;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.rsocket.autoconfigure.RSocketRequesterAutoConfiguration;
|
||||
import org.springframework.boot.rsocket.autoconfigure.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.security;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import graphql.schema.idl.TypeRuntimeWiring;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.Book;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlTestDataFetchers;
|
||||
import org.springframework.boot.graphql.autoconfigure.reactive.GraphQlWebFluxAutoConfiguration;
|
||||
import org.springframework.boot.http.codec.autoconfigure.CodecsAutoConfiguration;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.security.autoconfigure.reactive.ReactiveSecurityAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
|
||||
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.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.config.web.server.ServerHttpSecurity.CsrfSpec;
|
||||
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
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
@Disabled("Waiting on compatible release")
|
||||
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')")
|
||||
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(CsrfSpec::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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.security;
|
||||
|
||||
import graphql.schema.idl.TypeRuntimeWiring;
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.Book;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlTestDataFetchers;
|
||||
import org.springframework.boot.graphql.autoconfigure.servlet.GraphQlWebMvcAutoConfiguration;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
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.SecurityDataFetcherExceptionResolver;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
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.assertj.MockMvcTester;
|
||||
|
||||
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.post;
|
||||
|
||||
/**
|
||||
* Tests for {@link GraphQlWebMvcSecurityAutoConfiguration}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
@Disabled("Waiting on compatible release")
|
||||
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));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anonymousUserShouldBeUnauthorized() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
|
||||
assertThat(mvc.post().uri("/graphql").content("{\"query\": \"" + query + "\"}")).satisfies((result) -> {
|
||||
assertThat(result).hasStatusOk().hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON);
|
||||
assertThat(result).bodyJson()
|
||||
.doesNotHavePath("data.bookById.name")
|
||||
.extractingPath("errors[0].extensions.classification")
|
||||
.asString()
|
||||
.isEqualTo(ErrorType.UNAUTHORIZED.toString());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticatedUserShouldGetData() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}";
|
||||
assertThat(mvc.post().uri("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob")))
|
||||
.satisfies((result) -> {
|
||||
assertThat(result).hasStatusOk().hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON);
|
||||
assertThat(result).bodyJson()
|
||||
.doesNotHavePath("errors")
|
||||
.extractingPath("data.bookById.name")
|
||||
.asString()
|
||||
.isEqualTo("GraphQL for beginners");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void withMockMvc(ThrowingConsumer<MockMvcTester> mvc) {
|
||||
this.contextRunner.run((context) -> {
|
||||
MediaType mediaType = MediaType.APPLICATION_JSON;
|
||||
MockMvcTester mockMVc = MockMvcTester.from(context,
|
||||
(builder) -> builder.defaultRequest(post("/graphql").contentType(mediaType).accept(mediaType))
|
||||
.apply(springSecurity())
|
||||
.build());
|
||||
mvc.accept(mockMVc);
|
||||
});
|
||||
}
|
||||
|
||||
@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')")
|
||||
Book getBookdById(String id) {
|
||||
return GraphQlTestDataFetchers.getBookById(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity(prePostEnabled = true)
|
||||
@SuppressWarnings("deprecation")
|
||||
static class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
DefaultSecurityFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
|
||||
return http.csrf(CsrfConfigurer::disable)
|
||||
// Demonstrate that method security works
|
||||
// Best practice to use both for defense in depth
|
||||
.authorizeHttpRequests((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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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.graphql.autoconfigure.servlet;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.schema.idl.TypeRuntimeWiring;
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.graphql.autoconfigure.GraphQlTestDataFetchers;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
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.GraphQlSseHandler;
|
||||
import org.springframework.graphql.server.webmvc.GraphQlWebSocketHandler;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.support.RouterFunctionMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
|
||||
/**
|
||||
* Tests for {@link GraphQlWebMvcAutoConfiguration}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@WithResource(name = "graphql/types/book.graphqls", content = """
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
pageCount: Int
|
||||
author: String
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "graphql/schema.graphqls", content = """
|
||||
type Query {
|
||||
greeting(name: String! = "Spring"): String!
|
||||
bookById(id: ID): Book
|
||||
books: BookConnection
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
booksOnSale(minPages: Int) : Book!
|
||||
}
|
||||
""")
|
||||
@Disabled("Waiting on compatible release")
|
||||
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 shouldConfigureSseTimeout() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.http.sse.timeout=10s").run((context) -> {
|
||||
assertThat(context).hasSingleBean(GraphQlSseHandler.class);
|
||||
GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class);
|
||||
assertThat(handler).hasFieldOrPropertyWithValue("timeout", Duration.ofSeconds(10));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureSseKeepAlive() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.http.sse.keep-alive=5s").run((context) -> {
|
||||
assertThat(context).hasSingleBean(GraphQlSseHandler.class);
|
||||
GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class);
|
||||
assertThat(handler).hasFieldOrPropertyWithValue("keepAliveDuration", Duration.ofSeconds(5));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleQueryShouldWork() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
||||
assertThat(mvc.post().uri("/graphql").content("{\"query\": \"" + query + "\"}")).satisfies((result) -> {
|
||||
assertThat(result).hasStatusOk().hasContentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL_RESPONSE);
|
||||
assertThat(result).bodyJson()
|
||||
.extractingPath("data.bookById.name")
|
||||
.asString()
|
||||
.isEqualTo("GraphQL for beginners");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void SseSubscriptionShouldWork() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{ booksOnSale(minPages: 50){ id name pageCount author } }";
|
||||
assertThat(mvc.post()
|
||||
.uri("/graphql")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.content("{\"query\": \"subscription TestSubscription " + query + "\"}")).satisfies((result) -> {
|
||||
assertThat(result).hasStatusOk().hasContentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM);
|
||||
assertThat(result).bodyText()
|
||||
.containsSubsequence("event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-1\",\"name\":\"GraphQL for beginners\",\"pageCount\":100,\"author\":\"John GraphQL\"}}}",
|
||||
"event:next",
|
||||
"data:{\"data\":{\"booksOnSale\":{\"id\":\"book-2\",\"name\":\"Harry Potter and the Philosopher's Stone\",\"pageCount\":223,\"author\":\"Joanne Rowling\"}}}");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedContentTypeShouldBeRejected() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
||||
assertThat(mvc.post()
|
||||
.uri("/graphql")
|
||||
.content("{\"query\": \"" + query + "\"}")
|
||||
.contentType(MediaType.TEXT_PLAIN)).hasStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
.headers()
|
||||
.hasValue("Accept", "application/json");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpGetQueryShouldBeRejected() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
||||
assertThat(mvc.get().uri("/graphql?query={query}", "{\"query\": \"" + query + "\"}"))
|
||||
.hasStatus(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.headers()
|
||||
.hasValue("Allow", "POST");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMissingQuery() {
|
||||
withMockMvc((mvc) -> assertThat(mvc.post().uri("/graphql").content("{}")).hasStatus(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectQueryWithInvalidJson() {
|
||||
withMockMvc((mvc) -> assertThat(mvc.post().uri("/graphql").content(":)")).hasStatus(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureWebInterceptors() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
|
||||
assertThat(mvc.post().uri("/graphql").content("{\"query\": \"" + query + "\"}")).hasStatusOk()
|
||||
.headers()
|
||||
.hasValue("X-Custom-Header", "42");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeSchemaEndpoint() {
|
||||
withMockMvc((mvc) -> assertThat(mvc.get().uri("/graphql/schema")).hasStatusOk()
|
||||
.hasContentType(MediaType.TEXT_PLAIN)
|
||||
.bodyText()
|
||||
.contains("type Book"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeGraphiqlEndpoint() {
|
||||
withMockMvc((mvc) -> {
|
||||
assertThat(mvc.get().uri("/graphiql")).hasStatus3xxRedirection()
|
||||
.hasRedirectedUrl("http://localhost/graphiql?path=/graphql");
|
||||
assertThat(mvc.get().uri("/graphiql?path=/graphql")).hasStatusOk()
|
||||
.contentType()
|
||||
.isEqualTo(MediaType.TEXT_HTML);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportCors() {
|
||||
withMockMvc((mvc) -> {
|
||||
String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount"
|
||||
+ " author" + " }" + "}";
|
||||
assertThat(mvc.post()
|
||||
.uri("/graphql")
|
||||
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
|
||||
.header(HttpHeaders.ORIGIN, "https://example.com")
|
||||
.content("{\"query\": \"" + query + "\"}"))
|
||||
.satisfies((result) -> assertThat(result).hasStatusOk()
|
||||
.headers()
|
||||
.hasValue(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://example.com")
|
||||
.hasValue(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureWebSocketBeans() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws").run((context) -> {
|
||||
assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class);
|
||||
assertThat(context.getBeanProvider(HandlerMapping.class).orderedStream().toList()).containsSubsequence(
|
||||
context.getBean(WebSocketHandlerMapping.class), context.getBean(RouterFunctionMapping.class),
|
||||
context.getBean(RequestMappingHandlerMapping.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureWebSocketProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.graphql.websocket.path=/ws",
|
||||
"spring.graphql.websocket.connection-init-timeout=120s", "spring.graphql.websocket.keep-alive=30s")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class);
|
||||
GraphQlWebSocketHandler graphQlWebSocketHandler = context.getBean(GraphQlWebSocketHandler.class);
|
||||
assertThat(graphQlWebSocketHandler).extracting("initTimeoutDuration")
|
||||
.isEqualTo(Duration.ofSeconds(120));
|
||||
assertThat(graphQlWebSocketHandler).extracting("keepAliveDuration").isEqualTo(Duration.ofSeconds(30));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void routerFunctionShouldHaveOrderZero() {
|
||||
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]);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new GraphQlWebMvcAutoConfiguration.GraphiQlResourceHints().registerHints(hints, getClass().getClassLoader());
|
||||
assertThat(RuntimeHintsPredicates.resource().forResource("graphiql/index.html")).accepts(hints);
|
||||
}
|
||||
|
||||
private void withMockMvc(ThrowingConsumer<MockMvcTester> mvc) {
|
||||
this.contextRunner.run((context) -> {
|
||||
MockMvcTester mockMVc = MockMvcTester.from(context,
|
||||
(builder) -> builder
|
||||
.defaultRequest(post("/graphql").contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_GRAPHQL_RESPONSE))
|
||||
.build());
|
||||
mvc.accept(mockMVc);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DataFetchersConfiguration {
|
||||
|
||||
@Bean
|
||||
RuntimeWiringConfigurer bookDataFetcher() {
|
||||
return (builder) -> {
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Query")
|
||||
.dataFetcher("bookById", GraphQlTestDataFetchers.getBookByIdDataFetcher()));
|
||||
builder.type(TypeRuntimeWiring.newTypeWiring("Subscription")
|
||||
.dataFetcher("booksOnSale", GraphQlTestDataFetchers.getBooksOnSaleDataFetcher()));
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user