diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/AutoRegistrationRuntimeWiringConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/AutoRegistrationRuntimeWiringConfigurer.java new file mode 100644 index 000000000..5967bb7b5 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/AutoRegistrationRuntimeWiringConfigurer.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.graphql.data.query; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +import graphql.language.FieldDefinition; +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNamedOutputType; +import graphql.schema.GraphQLType; +import graphql.schema.idl.FieldWiringEnvironment; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.WiringFactory; + +import org.springframework.graphql.execution.RuntimeWiringConfigurer; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Given a map of GraphQL type names and DataFetcher factories, find queries + * with a matching return type and register DataFetcher's for them, unless they + * already have registrations. + * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +class AutoRegistrationRuntimeWiringConfigurer implements RuntimeWiringConfigurer { + + private final Map>> dataFetcherFactories; + + + public AutoRegistrationRuntimeWiringConfigurer( + Map>> dataFetcherFactories) { + + this.dataFetcherFactories = dataFetcherFactories; + } + + + @Override + public void configure(RuntimeWiring.Builder builder) { + } + + @Override + public void configure(RuntimeWiring.Builder builder, List container) { + container.add(new AutoRegistrationWiringFactory(builder)); + } + + + private class AutoRegistrationWiringFactory implements WiringFactory { + + private final RuntimeWiring.Builder builder; + + @Nullable + private Predicate existingQueryDataFetcherPredicate; + + AutoRegistrationWiringFactory(RuntimeWiring.Builder builder) { + this.builder = builder; + } + + @Override + public boolean providesDataFetcher(FieldWiringEnvironment environment) { + if (dataFetcherFactories.isEmpty()) { + return false; + } + + if (!environment.getParentType().getName().equals("Query")) { + return false; + } + + GraphQLType outputType = (environment.getFieldType() instanceof GraphQLList ? + ((GraphQLList) environment.getFieldType()).getWrappedType() : + environment.getFieldType()); + + String outputTypeName = (outputType instanceof GraphQLNamedOutputType ? + ((GraphQLNamedOutputType) outputType).getName() : null); + + return (outputTypeName != null && + dataFetcherFactories.containsKey(outputTypeName) && + !hasDataFetcherFor(environment.getFieldDefinition())); + } + + private boolean hasDataFetcherFor(FieldDefinition fieldDefinition) { + if (this.existingQueryDataFetcherPredicate == null) { + Map map = this.builder.build().getDataFetcherForType("Query"); + this.existingQueryDataFetcherPredicate = fieldName -> map.get(fieldName) != null; + } + return this.existingQueryDataFetcherPredicate.test(fieldDefinition.getName()); + } + + @Override + public DataFetcher getDataFetcher(FieldWiringEnvironment environment) { + return environment.getFieldType() instanceof GraphQLList ? + initDataFetcher(((GraphQLList) environment.getFieldType()).getWrappedType(), false) : + initDataFetcher(environment.getFieldType(), true); + } + + private DataFetcher initDataFetcher(GraphQLType type, boolean single) { + Assert.isInstanceOf(GraphQLNamedOutputType.class, type); + String typeName = ((GraphQLNamedOutputType) type).getName(); + Function> factory = dataFetcherFactories.get(typeName); + Assert.notNull(factory, "Expected DataFetcher factory"); + return factory.apply(single); + } + + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/AutoRegistrationTypeVisitor.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/AutoRegistrationTypeVisitor.java index e4b7740e8..fe1c1ade3 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/AutoRegistrationTypeVisitor.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/AutoRegistrationTypeVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,8 +39,9 @@ * already have registrations. * * @author Rossen Stoyanchev - * @since 1.0.0 + * @deprecated in favor of {@link AutoRegistrationRuntimeWiringConfigurer} */ +@Deprecated class AutoRegistrationTypeVisitor extends GraphQLTypeVisitorStub { private final Map>> dataFetcherFactories; diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java index 2f82e226e..8b3b467ca 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.springframework.data.util.TypeInformation; import org.springframework.graphql.data.GraphQlArgumentInitializer; import org.springframework.graphql.data.GraphQlRepository; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.util.Assert; /** @@ -70,9 +71,9 @@ * {@link QueryByExampleDataFetcher.ReactiveBuilder} for further options on * result projections and sorting. * - *

{@code QueryByExampleDataFetcher} {@link #autoRegistrationTypeVisitor(List, List) exposes} - * a {@link GraphQLTypeVisitor} that can auto-register repositories annotated with - * {@link GraphQlRepository @GraphQlRepository}. + *

{@code QueryByExampleDataFetcher} {@link #autoRegistrationConfigurer(List, List) exposes} + * a {@link RuntimeWiringConfigurer} that can auto-register repositories + * annotated with {@link GraphQlRepository @GraphQlRepository}. * * @param returned result type * @author Greg Turnquist @@ -150,6 +151,42 @@ public static ReactiveBuilder builder(ReactiveQueryByExampleExecutor(executor, RepositoryUtils.getDomainType(executor)); } + /** + * Return a {@link RuntimeWiringConfigurer} that installs a + * {@link graphql.schema.idl.WiringFactory} to find queries with a return + * type whose name matches to the domain type name of the given repositories + * and registers {@link DataFetcher}s for them. + * + *

Note: This applies only to top-level queries and + * repositories annotated with {@link GraphQlRepository @GraphQlRepository}. + * + * @param executors repositories to consider for registration + * @param reactiveExecutors reactive repositories to consider for registration + * @return the created configurer + */ + public static RuntimeWiringConfigurer autoRegistrationConfigurer( + List> executors, + List> reactiveExecutors) { + + Map>> factories = new HashMap<>(); + + for (QueryByExampleExecutor executor : executors) { + String typeName = RepositoryUtils.getGraphQlTypeName(executor); + if (typeName != null) { + factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many()); + } + } + + for (ReactiveQueryByExampleExecutor executor : reactiveExecutors) { + String typeName = RepositoryUtils.getGraphQlTypeName(executor); + if (typeName != null) { + factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many()); + } + } + + return new AutoRegistrationRuntimeWiringConfigurer(factories); + } + /** * Create a {@link GraphQLTypeVisitor} that finds queries with a return type * whose name matches to the domain type name of the given repositories and @@ -157,10 +194,11 @@ public static ReactiveBuilder builder(ReactiveQueryByExampleExecutorNote: currently, this method will match only to * queries under the top-level "Query" type in the GraphQL schema. * - * @param executors repositories to consider for registration + * @param executors repositories to consider for registration * @param reactiveExecutors reactive repositories to consider for registration * @return the created visitor */ + @SuppressWarnings("deprecation") public static GraphQLTypeVisitor autoRegistrationTypeVisitor( List> executors, List> reactiveExecutors) { diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java index 90db8aa88..d0f992d63 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.graphql.data.GraphQlRepository; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -77,9 +78,9 @@ * options on GraphQL Query argument to Querydsl Predicate binding customizations, * result projections, and sorting. * - *

{@code QuerydslDataFetcher} {@link #autoRegistrationTypeVisitor(List, List) exposes} - * a {@link GraphQLTypeVisitor} that can auto-register repositories annotated with - * {@link GraphQlRepository @GraphQlRepository}. + *

{@code QuerydslDataFetcher} {@link #autoRegistrationConfigurer(List, List) exposes} + * a {@link RuntimeWiringConfigurer} that can auto-register repositories + * annotated with {@link GraphQlRepository @GraphQlRepository}. * * @param returned result type * @author Mark Paluch @@ -173,6 +174,48 @@ public static ReactiveBuilder builder(ReactiveQuerydslPredicateExecuto return new ReactiveBuilder<>(executor, RepositoryUtils.getDomainType(executor)); } + /** + * Return a {@link RuntimeWiringConfigurer} that installs a + * {@link graphql.schema.idl.WiringFactory} to find queries with a return + * type whose name matches to the domain type name of the given repositories + * and registers {@link DataFetcher}s for them. + * + *

Note: This applies only to top-level queries and + * repositories annotated with {@link GraphQlRepository @GraphQlRepository}. + * If a repository is also an instance of {@link QuerydslBinderCustomizer}, + * this is transparently detected and applied through the + * {@code QuerydslDataFetcher} builder methods. + * + * @param executors repositories to consider for registration + * @param reactiveExecutors reactive repositories to consider for registration + * @return the created configurer + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static RuntimeWiringConfigurer autoRegistrationConfigurer( + List> executors, + List> reactiveExecutors) { + + Map>> factories = new HashMap<>(); + + for (QuerydslPredicateExecutor executor : executors) { + String typeName = RepositoryUtils.getGraphQlTypeName(executor); + if (typeName != null) { + Builder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor)); + factories.put(typeName, single -> single ? builder.single() : builder.many()); + } + } + + for (ReactiveQuerydslPredicateExecutor executor : reactiveExecutors) { + String typeName = RepositoryUtils.getGraphQlTypeName(executor); + if (typeName != null) { + ReactiveBuilder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor)); + factories.put(typeName, single -> single ? builder.single() : builder.many()); + } + } + + return new AutoRegistrationRuntimeWiringConfigurer(factories); + } + /** * Return a {@link GraphQLTypeVisitor} that auto-registers the given * Querydsl repositories for queries that do not already have a registered @@ -188,8 +231,10 @@ public static ReactiveBuilder builder(ReactiveQuerydslPredicateExecuto * @param executors repositories to consider for registration * @param reactiveExecutors reactive repositories to consider for registration * @return the created visitor + * @deprecated in favor of {@link #autoRegistrationConfigurer(List, List)} */ @SuppressWarnings({"unchecked", "rawtypes"}) + @Deprecated public static GraphQLTypeVisitor autoRegistrationTypeVisitor( List> executors, List> reactiveExecutors) { @@ -291,7 +336,7 @@ public Builder sortBy(Sort sort) { * *

If a Querydsl repository implements {@link QuerydslBinderCustomizer} * itself, this is automatically detected and applied during - * {@link #autoRegistrationTypeVisitor(List, List) auto-registration}. + * {@link #autoRegistrationConfigurer(List, List) auto-registration}. * For manual registration, you will need to use this method to apply it. * * @param customizer to customize the GraphQL query to Querydsl @@ -394,7 +439,7 @@ public ReactiveBuilder sortBy(Sort sort) { * *

If a Querydsl repository implements {@link QuerydslBinderCustomizer} * itself, this is automatically detected and applied during - * {@link #autoRegistrationTypeVisitor(List, List) auto-registration}. + * {@link #autoRegistrationConfigurer(List, List) auto-registration}. * For manual registration, you will need to use this method to apply it. * * @param customizer to customize the GraphQL query to Querydsl diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java index 3ea3ebc8f..7120458b0 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import com.querydsl.core.types.Predicate; import graphql.schema.DataFetcher; -import graphql.schema.GraphQLTypeVisitor; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import reactor.core.publisher.Flux; @@ -47,6 +46,7 @@ import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.GraphQlSetup; import org.springframework.graphql.data.GraphQlRepository; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; @@ -265,11 +265,11 @@ private static GraphQlSetup initGraphQlSetup( @Nullable QuerydslPredicateExecutor executor, @Nullable ReactiveQuerydslPredicateExecutor reactiveExecutor) { - GraphQLTypeVisitor visitor = QuerydslDataFetcher.autoRegistrationTypeVisitor( + RuntimeWiringConfigurer configurer = QuerydslDataFetcher.autoRegistrationConfigurer( (executor != null ? Collections.singletonList(executor) : Collections.emptyList()), (reactiveExecutor != null ? Collections.singletonList(reactiveExecutor) : Collections.emptyList())); - return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor); + return GraphQlSetup.schemaResource(BookSource.schema).runtimeWiring(configurer); } private WebInput input(String query) { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java index caad7186f..da54c4508 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import javax.sql.DataSource; import graphql.schema.DataFetcher; -import graphql.schema.GraphQLTypeVisitor; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -44,6 +43,7 @@ import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.GraphQlSetup; import org.springframework.graphql.data.query.QueryByExampleDataFetcher; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; @@ -179,11 +179,11 @@ private static GraphQlSetup graphQlSetup(@Nullable QueryByExampleExecutor exe private static GraphQlSetup initGraphQlSetup(@Nullable QueryByExampleExecutor executor) { - GraphQLTypeVisitor visitor = QueryByExampleDataFetcher.autoRegistrationTypeVisitor( + RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher.autoRegistrationConfigurer( executor != null ? Collections.singletonList(executor) : Collections.emptyList(), Collections.emptyList()); - return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor); + return GraphQlSetup.schemaResource(BookSource.schema).runtimeWiring(configurer); } private WebInput input(String query) { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java index ec9728271..98fb6f825 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherMongoDbTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import com.mongodb.client.MongoClients; import graphql.schema.DataFetcher; -import graphql.schema.GraphQLTypeVisitor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.testcontainers.containers.MongoDBContainer; @@ -46,6 +45,7 @@ import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.GraphQlSetup; import org.springframework.graphql.data.query.QueryByExampleDataFetcher; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; @@ -177,11 +177,11 @@ private static GraphQlSetup graphQlSetup(@Nullable QueryByExampleExecutor exe private static GraphQlSetup initGraphQlSetup(@Nullable QueryByExampleExecutor executor) { - GraphQLTypeVisitor visitor = QueryByExampleDataFetcher.autoRegistrationTypeVisitor( + RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher.autoRegistrationConfigurer( (executor != null ? Collections.singletonList(executor) : Collections.emptyList()), Collections.emptyList()); - return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor); + return GraphQlSetup.schemaResource(BookSource.schema).runtimeWiring(configurer); } private WebInput input(String query) { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java index e2b76d3ae..5b8efd5d6 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/mongo/QueryByExampleDataFetcherReactiveMongoDbTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import com.mongodb.reactivestreams.client.MongoClients; import graphql.schema.DataFetcher; -import graphql.schema.GraphQLTypeVisitor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.testcontainers.containers.MongoDBContainer; @@ -45,6 +44,7 @@ import org.springframework.graphql.GraphQlResponse; import org.springframework.graphql.GraphQlSetup; import org.springframework.graphql.data.query.QueryByExampleDataFetcher; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.graphql.web.WebGraphQlHandler; import org.springframework.graphql.web.WebInput; import org.springframework.graphql.web.WebOutput; @@ -149,11 +149,11 @@ private static GraphQlSetup graphQlSetup(@Nullable ReactiveQueryByExampleExecuto private static GraphQlSetup initGraphQlSetup(@Nullable ReactiveQueryByExampleExecutor executor) { - GraphQLTypeVisitor visitor = QueryByExampleDataFetcher.autoRegistrationTypeVisitor( + RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher.autoRegistrationConfigurer( Collections.emptyList(), (executor != null ? Collections.singletonList(executor) : Collections.emptyList())); - return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor); + return GraphQlSetup.schemaResource(BookSource.schema).runtimeWiring(configurer); } private WebInput input(String query) {