From c148eb40b4a2e72c609b86d2a8fcbb98b208b83a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 19 Jul 2021 07:40:33 +0100 Subject: [PATCH] Auto registration of Querydsl DataFetcher's See gh-93 --- graphql-spring-boot-starter/build.gradle | 3 + ...aphQlWebFluxQuerydslAutoConfiguration.java | 70 +++++++ ...raphQlWebMvcQuerydslAutoConfiguration.java | 74 ++++++++ .../graphql/boot/data/package-info.java | 25 +++ .../main/resources/META-INF/spring.factories | 4 +- .../ArtifactRepositoryDataWiring.java | 25 --- .../graphql/data/QuerydslDataFetcher.java | 113 ++++++++++++ .../DefaultGraphQlSourceBuilder.java | 6 +- .../data/QuerydslDataFetcherTests.java | 171 +++++++++++++----- 9 files changed, 419 insertions(+), 72 deletions(-) create mode 100644 graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebFluxQuerydslAutoConfiguration.java create mode 100644 graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebMvcQuerydslAutoConfiguration.java create mode 100644 graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/package-info.java delete mode 100644 samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java diff --git a/graphql-spring-boot-starter/build.gradle b/graphql-spring-boot-starter/build.gradle index aaca0848..ed53a1d3 100644 --- a/graphql-spring-boot-starter/build.gradle +++ b/graphql-spring-boot-starter/build.gradle @@ -41,6 +41,9 @@ dependencies { compileOnly 'org.springframework.security:spring-security-config' compileOnly 'org.springframework.security:spring-security-web' + compileOnly 'com.querydsl:querydsl-core:4.4.0' + compileOnly 'org.springframework.data:spring-data-commons' + compileOnly project(':spring-graphql-test') compileOnly 'org.springframework.boot:spring-boot-test' diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebFluxQuerydslAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebFluxQuerydslAutoConfiguration.java new file mode 100644 index 00000000..2a4659b5 --- /dev/null +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebFluxQuerydslAutoConfiguration.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.boot.data; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import graphql.GraphQL; +import graphql.schema.GraphQLTypeVisitor; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +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.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.graphql.boot.GraphQlAutoConfiguration; +import org.springframework.graphql.boot.GraphQlSourceBuilderCustomizer; +import org.springframework.graphql.data.QuerydslDataFetcher; +import org.springframework.graphql.execution.GraphQlSource; + +/** + * {@link EnableAutoConfiguration Auto-configuration} that creates a + * {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories + * with Querydsl support and register them as {@code DataFetcher}s. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnClass({GraphQL.class, QuerydslPredicateExecutor.class }) +@ConditionalOnBean(GraphQlSource.Builder.class) +@AutoConfigureAfter(GraphQlAutoConfiguration.class) +public class GraphQlWebFluxQuerydslAutoConfiguration { + + @Bean + public GraphQlSourceBuilderCustomizer reactiveQuerydslRegistrar( + ObjectProvider> executorsProvider) { + + return builder -> { + List> executors = + executorsProvider.stream().collect(Collectors.toList()); + + if (!executors.isEmpty()) { + GraphQLTypeVisitor visitor = QuerydslDataFetcher.registrationTypeVisitor(Collections.emptyList(), executors); + builder.typeVisitors(Collections.singletonList(visitor)); + } + }; + } + +} diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebMvcQuerydslAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebMvcQuerydslAutoConfiguration.java new file mode 100644 index 00000000..e056d923 --- /dev/null +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/GraphQlWebMvcQuerydslAutoConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.boot.data; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import graphql.GraphQL; +import graphql.schema.GraphQLTypeVisitor; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +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.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.graphql.boot.GraphQlAutoConfiguration; +import org.springframework.graphql.boot.GraphQlSourceBuilderCustomizer; +import org.springframework.graphql.data.QuerydslDataFetcher; +import org.springframework.graphql.execution.GraphQlSource; + +/** + * {@link EnableAutoConfiguration Auto-configuration} that creates a + * {@link GraphQlSourceBuilderCustomizer}s to detect Spring Data repositories + * with Querydsl support and register them as {@code DataFetcher}s. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass({GraphQL.class, QuerydslPredicateExecutor.class }) +@ConditionalOnBean(GraphQlSource.Builder.class) +@AutoConfigureAfter(GraphQlAutoConfiguration.class) +public class GraphQlWebMvcQuerydslAutoConfiguration { + + @Bean + public GraphQlSourceBuilderCustomizer querydslRegistrar( + ObjectProvider> executorsProvider, + ObjectProvider> reactiveExecutorsProvider) { + + return builder -> { + List> executors = + executorsProvider.stream().collect(Collectors.toList()); + + List> reactiveExecutors = + reactiveExecutorsProvider.stream().collect(Collectors.toList()); + + if (!executors.isEmpty()) { + GraphQLTypeVisitor visitor = QuerydslDataFetcher.registrationTypeVisitor(executors, reactiveExecutors); + builder.typeVisitors(Collections.singletonList(visitor)); + } + }; + } + +} diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/package-info.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/package-info.java new file mode 100644 index 00000000..76fb4d68 --- /dev/null +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/data/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration classes for data integrations. + */ +@NonNullApi +@NonNullFields +package org.springframework.graphql.boot.data; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories b/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories index 394c4cf5..4ed3b560 100644 --- a/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/graphql-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -5,7 +5,9 @@ org.springframework.graphql.boot.GraphQlWebFluxAutoConfiguration,\ org.springframework.graphql.boot.GraphQlWebMvcAutoConfiguration,\ org.springframework.graphql.boot.actuate.metrics.GraphQlMetricsAutoConfiguration,\ org.springframework.graphql.boot.security.GraphQlWebFluxSecurityAutoConfiguration,\ -org.springframework.graphql.boot.security.GraphQlWebMvcSecurityAutoConfiguration +org.springframework.graphql.boot.security.GraphQlWebMvcSecurityAutoConfiguration,\ +org.springframework.graphql.boot.data.GraphQlWebMvcQuerydslAutoConfiguration,\ +org.springframework.graphql.boot.data.GraphQlWebFluxQuerydslAutoConfiguration # Spring Test @AutoConfigureGraphQlTester org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester=\ diff --git a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java b/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java deleted file mode 100644 index 0bb8de64..00000000 --- a/samples/webmvc-http/src/main/java/io/spring/sample/graphql/repository/ArtifactRepositoryDataWiring.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.spring.sample.graphql.repository; - -import graphql.schema.idl.RuntimeWiring; - -import org.springframework.graphql.boot.RuntimeWiringBuilderCustomizer; -import org.springframework.graphql.data.QuerydslDataFetcher; -import org.springframework.stereotype.Component; - -@Component -public class ArtifactRepositoryDataWiring implements RuntimeWiringBuilderCustomizer { - - private final ArtifactRepositories repositories; - - public ArtifactRepositoryDataWiring(ArtifactRepositories repositories) { - this.repositories = repositories; - } - - @Override - public void customize(RuntimeWiring.Builder builder) { - builder.type("Query", typeWiring -> typeWiring - .dataFetcher("artifactRepositories", QuerydslDataFetcher.builder(repositories).many()) - .dataFetcher("artifactRepository", QuerydslDataFetcher.builder(repositories).single())); - } - -} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java index 599cdd22..1e2bde70 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/QuerydslDataFetcher.java @@ -18,6 +18,8 @@ package org.springframework.graphql.data; import java.lang.reflect.Type; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; @@ -26,6 +28,18 @@ import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.Predicate; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLFieldsContainer; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNamedOutputType; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeVisitor; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.PropertyDataFetcher; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -43,10 +57,12 @@ import org.springframework.data.querydsl.binding.QuerydslBindings; import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -144,6 +160,21 @@ public abstract class QuerydslDataFetcher { (bindings, root) -> {}, Function.identity()); } + /** + * Create a {@link GraphQLTypeVisitor} that finds fields whose type matches + * the domain type of the the given repositories and registers them as + * {@link DataFetcher}s. + * @param executors repositories to consider for registration + * @param reactiveExecutors reactive repositories to consider for registration + * @return the created visitor + */ + public static GraphQLTypeVisitor registrationTypeVisitor( + List> executors, + List> reactiveExecutors) { + + return new RegistrationTypeVisitor(executors, reactiveExecutors); + } + @SuppressWarnings({"unchecked", "rawtypes"}) protected Predicate buildPredicate(DataFetchingEnvironment environment) { MultiValueMap parameters = new LinkedMultiValueMap<>(); @@ -453,4 +484,86 @@ public abstract class QuerydslDataFetcher { } + + /** + * Visitor that auto-registers Querydsl repositories. + */ + private static class RegistrationTypeVisitor extends GraphQLTypeVisitorStub { + + private final Map>> executorMap; + + RegistrationTypeVisitor( + List> executors, + List> reactiveExecutors) { + + this.executorMap = initExecutorMap(executors, reactiveExecutors); + } + + private Map>> initExecutorMap( + List> executors, + List> reactiveExecutors) { + + int size = executors.size() + reactiveExecutors.size(); + Map>> map = new HashMap<>(size); + + for (QuerydslPredicateExecutor executor : executors) { + Class repositoryInterface = getRepositoryInterface(executor); + RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + map.put(metadata.getDomainType().getSimpleName(), (single) -> single ? + QuerydslDataFetcher.builder(executor).single() : + QuerydslDataFetcher.builder(executor).many()); + } + + for (ReactiveQuerydslPredicateExecutor reactiveExecutor : reactiveExecutors) { + Class repositoryInterface = getRepositoryInterface(reactiveExecutor); + RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); + map.put(metadata.getDomainType().getSimpleName(), (single) -> single ? + QuerydslDataFetcher.builder(reactiveExecutor).single() : + QuerydslDataFetcher.builder(reactiveExecutor).many()); + } + + return map; + } + + @Override + public TraversalControl visitGraphQLFieldDefinition( + GraphQLFieldDefinition fieldDefinition, TraverserContext context) { + + GraphQLType fieldType = fieldDefinition.getType(); + DataFetcher dataFetcher = (fieldType instanceof GraphQLList ? + getDataFetcher(((GraphQLList) fieldType).getWrappedType(), false) : + getDataFetcher(fieldType, true)); + + if (dataFetcher != null) { + GraphQLFieldsContainer parent = (GraphQLFieldsContainer) context.getParentNode(); + GraphQLCodeRegistry.Builder registry = context.getVarFromParents(GraphQLCodeRegistry.Builder.class); + if (!hasDataFetcher(registry, parent, fieldDefinition)) { + registry.dataFetcher(parent, fieldDefinition, dataFetcher); + } + } + + return TraversalControl.CONTINUE; + } + + @Nullable + private DataFetcher getDataFetcher(GraphQLType type, boolean single) { + if (type instanceof GraphQLNamedOutputType) { + String typeName = ((GraphQLNamedOutputType) type).getName(); + Function> factory = this.executorMap.get(typeName); + if (factory != null) { + return factory.apply(single); + } + } + return null; + } + + private boolean hasDataFetcher( + GraphQLCodeRegistry.Builder registry, GraphQLFieldsContainer parent, + GraphQLFieldDefinition fieldDefinition) { + + DataFetcher fetcher = registry.getDataFetcher(parent, fieldDefinition); + return (fetcher != null && !(fetcher instanceof PropertyDataFetcher)); + } + } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java index 462b21ff..14e749f6 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultGraphQlSourceBuilder.java @@ -59,9 +59,6 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { private Consumer graphQlConfigurers = (builder) -> { }; - DefaultGraphQlSourceBuilder() { - this.typeVisitors.add(ContextDataFetcherDecorator.TYPE_VISITOR); - } @Override public GraphQlSource.Builder schemaResources(Resource... resources) { @@ -111,6 +108,9 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder { schema = SchemaTransformer.transformSchema(schema, visitor); } + // This comes last, wraps other DataFetcher's + schema = SchemaTransformer.transformSchema(schema, ContextDataFetcherDecorator.TYPE_VISITOR); + GraphQL.Builder builder = GraphQL.newGraphQL(schema); builder.defaultDataFetcherExceptionHandler(new ExceptionResolversExceptionHandler(this.exceptionResolvers)); if (!this.instrumentations.isEmpty()) { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java index 89ce8629..09b79af3 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/QuerydslDataFetcherTests.java @@ -16,16 +16,15 @@ package org.springframework.graphql.data; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.Consumer; import com.querydsl.core.types.Predicate; +import graphql.schema.GraphQLTypeVisitor; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.TypeRuntimeWiring; import org.junit.jupiter.api.Test; @@ -45,6 +44,13 @@ import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Unit tests for {@link QuerydslDataFetcher}. @@ -57,15 +63,24 @@ class QuerydslDataFetcherTests { Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); - WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("bookById", QuerydslDataFetcher.builder(mockRepository).single())); + BiConsumer, QuerydslPredicateExecutor> tester = + (wiringConfigurer, executor) -> { + WebGraphQlHandler handler = initWebGraphQlHandler(wiringConfigurer, executor, null); + WebOutput output = handler.handle(input("{ bookById(id: 1) {name}}")).block(); - WebOutput output = handler.handle(input("{ bookById(id: 1) {name}}")).block(); + // TODO: getData interferes with method overrides + assertThat((Object) output.getData()).isEqualTo( + Collections.singletonMap("bookById", + Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); + }; - // TODO: getData interferes with method overrides - assertThat((Object) output.getData()).isEqualTo( - Collections.singletonMap("bookById", - Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); + // explicit wiring + tester.accept( + builder -> builder.dataFetcher("bookById", QuerydslDataFetcher.builder(mockRepository).single()), + null); + + // auto registration + tester.accept(null, mockRepository); } @Test @@ -75,15 +90,48 @@ class QuerydslDataFetcherTests { Book book2 = new Book(53L, "Breaking Bad", "Heisenberg"); when(mockRepository.findAll(any(Predicate.class))).thenReturn(Arrays.asList(book1, book2)); - WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("books", QuerydslDataFetcher.builder(mockRepository).many())); + BiConsumer, QuerydslPredicateExecutor> tester = + (wiringConfigurer, executor) -> { + WebGraphQlHandler handler = initWebGraphQlHandler(wiringConfigurer, mockRepository, null); + WebOutput output = handler.handle(input("{ books {name}}")).block(); - WebOutput output = handler.handle(input("{ books {name}}")).block(); + assertThat((Object) output.getData()).isEqualTo( + Collections.singletonMap("books", Arrays.asList( + Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"), + Collections.singletonMap("name", "Breaking Bad")))); + }; + + // explicit wiring + tester.accept( + builder -> builder.dataFetcher("books", QuerydslDataFetcher.builder(mockRepository).many()), + null); + + // auto registration + tester.accept(null, mockRepository); + } + + @Test + void shouldFavorExplicitWiring() { + MockRepository mockRepository = mock(MockRepository.class); + Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); + when(mockRepository.findOne(any())).thenReturn(Optional.of(book)); + + // 1) Automatic registration only + WebGraphQlHandler handler = initWebGraphQlHandler(null, mockRepository, null); + WebOutput output = handler.handle(input("{ bookById(id: 1) {name}}")).block(); assertThat((Object) output.getData()).isEqualTo( - Collections.singletonMap("books", Arrays.asList( - Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"), - Collections.singletonMap("name", "Breaking Bad")))); + Collections.singletonMap("bookById", Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); + + // 2) Automatic registration and explicit wiring + handler = initWebGraphQlHandler( + builder -> builder.dataFetcher("bookById", env -> new Book(53L, "Breaking Bad", "Heisenberg")), + mockRepository, null); + + output = handler.handle(input("{ bookById(id: 1) {name}}")).block(); + + assertThat((Object) output.getData()).isEqualTo( + Collections.singletonMap("bookById", Collections.singletonMap("name", "Breaking Bad"))); } @Test @@ -132,8 +180,7 @@ class QuerydslDataFetcherTests { .dataFetcher("books", QuerydslDataFetcher .builder(mockRepository) .customizer((QuerydslBinderCustomizer) (bindings, book) -> - bindings.bind(book.name) - .firstOptional((path, value) -> value.map(path::startsWith))) + bindings.bind(book.name).firstOptional((path, value) -> value.map(path::startsWith))) .many())); handler.handle(input("{ books(name: \"H\", author: \"Doug\") {name}}")).block(); @@ -143,7 +190,6 @@ class QuerydslDataFetcherTests { verify(mockRepository).findAll(predicateCaptor.capture()); Predicate predicate = predicateCaptor.getValue(); - assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H").and(QBook.book.author.eq("Doug"))); } @@ -153,15 +199,24 @@ class QuerydslDataFetcherTests { Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams"); when(mockRepository.findOne(any())).thenReturn(Mono.just(book)); - WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("bookById", QuerydslDataFetcher.builder(mockRepository).single())); + BiConsumer, ReactiveQuerydslPredicateExecutor> tester = + (wiringConfigurer, executor) -> { + WebGraphQlHandler handler = initWebGraphQlHandler(wiringConfigurer, null, executor); + WebOutput output = handler.handle(input("{ bookById(id: 1) {name}}")).block(); - WebOutput output = handler.handle(input("{ bookById(id: 1) {name}}")).block(); + // TODO: getData interferes with method overrides + assertThat((Object) output.getData()).isEqualTo( + Collections.singletonMap("bookById", + Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); + }; - // TODO: getData interferes with method overries - assertThat((Object) output.getData()).isEqualTo( - Collections.singletonMap("bookById", - Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"))); + // explicit wiring + tester.accept( + builder -> builder.dataFetcher("bookById", QuerydslDataFetcher.builder(mockRepository).single()), + null); + + // auto registration + tester.accept(null, mockRepository); } @Test @@ -171,17 +226,27 @@ class QuerydslDataFetcherTests { Book book2 = new Book(53L, "Breaking Bad", "Heisenberg"); when(mockRepository.findAll((Predicate) any())).thenReturn(Flux.just(book1, book2)); - WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder - .dataFetcher("books", QuerydslDataFetcher.builder(mockRepository).many())); + BiConsumer, ReactiveQuerydslPredicateExecutor> tester = + (wiringConfigurer, executor) -> { + WebGraphQlHandler handler = initWebGraphQlHandler(wiringConfigurer, null, mockRepository); + WebOutput output = handler.handle(input("{ books {name}}")).block(); - WebOutput output = handler.handle(input("{ books {name}}")).block(); + assertThat((Object) output.getData()).isEqualTo( + Collections.singletonMap("books", Arrays.asList( + Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"), + Collections.singletonMap("name", "Breaking Bad")))); + }; - assertThat((Object) output.getData()).isEqualTo( - Collections.singletonMap("books", Arrays.asList( - Collections.singletonMap("name", "Hitchhiker's Guide to the Galaxy"), - Collections.singletonMap("name", "Breaking Bad")))); + // explicit wiring + tester.accept( + builder -> builder.dataFetcher("books", QuerydslDataFetcher.builder(mockRepository).many()), + null); + + // auto registration + tester.accept(null, mockRepository); } + interface MockRepository extends Repository, QuerydslPredicateExecutor { } @@ -191,20 +256,40 @@ class QuerydslDataFetcherTests { } static WebGraphQlHandler initWebGraphQlHandler(Consumer configurer) { + return initWebGraphQlHandler(configurer, null, null); + } + + static WebGraphQlHandler initWebGraphQlHandler( + @Nullable Consumer configurer, + @Nullable QuerydslPredicateExecutor executor, + @Nullable ReactiveQuerydslPredicateExecutor reactiveExecutor) { + return WebGraphQlHandler - .builder(new ExecutionGraphQlService(graphQlSource(configurer))) + .builder(new ExecutionGraphQlService(graphQlSource(configurer, executor, reactiveExecutor))) .build(); } - private static GraphQlSource graphQlSource(Consumer configurer) { - RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring(); - TypeRuntimeWiring.Builder wiringBuilder = TypeRuntimeWiring.newTypeWiring("Query"); - configurer.accept(wiringBuilder); - builder.type(wiringBuilder); - return GraphQlSource.builder() - .schemaResources(new ClassPathResource("books/schema.graphqls")) - .runtimeWiring(builder.build()) - .build(); + private static GraphQlSource graphQlSource( + @Nullable Consumer configurer, + @Nullable QuerydslPredicateExecutor executor, + @Nullable ReactiveQuerydslPredicateExecutor reactiveExecutor) { + + GraphQlSource.Builder graphQlSourceBuilder = GraphQlSource.builder() + .schemaResources(new ClassPathResource("books/schema.graphqls")); + + if (configurer != null) { + TypeRuntimeWiring.Builder typeBuilder = TypeRuntimeWiring.newTypeWiring("Query"); + configurer.accept(typeBuilder); + graphQlSourceBuilder.runtimeWiring(RuntimeWiring.newRuntimeWiring().type(typeBuilder).build()); + } + + GraphQLTypeVisitor visitor = QuerydslDataFetcher.registrationTypeVisitor( + (executor != null ? Collections.singletonList(executor) : Collections.emptyList()), + (reactiveExecutor != null ? Collections.singletonList(reactiveExecutor): Collections.emptyList())); + + graphQlSourceBuilder.typeVisitors(Collections.singletonList(visitor)); + + return graphQlSourceBuilder.build(); } private WebInput input(String query) {