Refactor auto-registration of Querydsl and QBE fetchers

Replace the TypeVisitor with a WiringFactory that checks for existing
registrations in RuntimeWiring.Builder.

Closes gh-244
This commit is contained in:
rstoyanchev
2022-01-14 19:00:36 +00:00
parent 2ecac0bd35
commit 5ff3f8b18d
8 changed files with 237 additions and 29 deletions

View File

@@ -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<String, Function<Boolean, DataFetcher<?>>> dataFetcherFactories;
public AutoRegistrationRuntimeWiringConfigurer(
Map<String, Function<Boolean, DataFetcher<?>>> dataFetcherFactories) {
this.dataFetcherFactories = dataFetcherFactories;
}
@Override
public void configure(RuntimeWiring.Builder builder) {
}
@Override
public void configure(RuntimeWiring.Builder builder, List<WiringFactory> container) {
container.add(new AutoRegistrationWiringFactory(builder));
}
private class AutoRegistrationWiringFactory implements WiringFactory {
private final RuntimeWiring.Builder builder;
@Nullable
private Predicate<String> 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<String, ?> 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<Boolean, DataFetcher<?>> factory = dataFetcherFactories.get(typeName);
Assert.notNull(factory, "Expected DataFetcher factory");
return factory.apply(single);
}
}
}

View File

@@ -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 @@ import org.springframework.lang.Nullable;
* already have registrations.
*
* @author Rossen Stoyanchev
* @since 1.0.0
* @deprecated in favor of {@link AutoRegistrationRuntimeWiringConfigurer}
*/
@Deprecated
class AutoRegistrationTypeVisitor extends GraphQLTypeVisitorStub {
private final Map<String, Function<Boolean, DataFetcher<?>>> dataFetcherFactories;

View File

@@ -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.ClassTypeInformation;
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 @@ import org.springframework.util.Assert;
* {@link QueryByExampleDataFetcher.ReactiveBuilder} for further options on
* result projections and sorting.
*
* <p>{@code QueryByExampleDataFetcher} {@link #autoRegistrationTypeVisitor(List, List) exposes}
* a {@link GraphQLTypeVisitor} that can auto-register repositories annotated with
* {@link GraphQlRepository @GraphQlRepository}.
* <p>{@code QueryByExampleDataFetcher} {@link #autoRegistrationConfigurer(List, List) exposes}
* a {@link RuntimeWiringConfigurer} that can auto-register repositories
* annotated with {@link GraphQlRepository @GraphQlRepository}.
*
* @param <T> returned result type
* @author Greg Turnquist
@@ -150,6 +151,42 @@ public abstract class QueryByExampleDataFetcher<T> {
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.
*
* <p><strong>Note:</strong> 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<QueryByExampleExecutor<?>> executors,
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
Map<String, Function<Boolean, DataFetcher<?>>> 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 abstract class QueryByExampleDataFetcher<T> {
* <p><strong>Note:</strong> 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<QueryByExampleExecutor<?>> executors,
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {

View File

@@ -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.repository.query.FluentQuery.FetchableFluentQuer
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 @@ import org.springframework.util.MultiValueMap;
* options on GraphQL Query argument to Querydsl Predicate binding customizations,
* result projections, and sorting.
*
* <p>{@code QuerydslDataFetcher} {@link #autoRegistrationTypeVisitor(List, List) exposes}
* a {@link GraphQLTypeVisitor} that can auto-register repositories annotated with
* {@link GraphQlRepository @GraphQlRepository}.
* <p>{@code QuerydslDataFetcher} {@link #autoRegistrationConfigurer(List, List) exposes}
* a {@link RuntimeWiringConfigurer} that can auto-register repositories
* annotated with {@link GraphQlRepository @GraphQlRepository}.
*
* @param <T> returned result type
* @author Mark Paluch
@@ -173,6 +174,48 @@ public abstract class QuerydslDataFetcher<T> {
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.
*
* <p><strong>Note:</strong> 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<QuerydslPredicateExecutor<?>> executors,
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
Map<String, Function<Boolean, DataFetcher<?>>> 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 abstract class QuerydslDataFetcher<T> {
* @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<QuerydslPredicateExecutor<?>> executors,
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
@@ -291,7 +336,7 @@ public abstract class QuerydslDataFetcher<T> {
*
* <p>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 abstract class QuerydslDataFetcher<T> {
*
* <p>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

View File

@@ -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 java.util.stream.Collectors;
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.BookSource;
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 @@ class QuerydslDataFetcherTests {
@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) {

View File

@@ -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.persistence.EntityManagerFactory;
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.BookSource;
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 @@ class QueryByExampleDataFetcherJpaTests {
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) {

View File

@@ -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 java.util.stream.Collectors;
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.BookSource;
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 @@ class QueryByExampleDataFetcherMongoDbTests {
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) {

View File

@@ -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 java.util.stream.Collectors;
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.BookSource;
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 @@ class QueryByExampleDataFetcherReactiveMongoDbTests {
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) {