Skip to content

Commit

Permalink
Refactor auto-registration of Querydsl and QBE fetchers
Browse files Browse the repository at this point in the history
Replace the TypeVisitor with a WiringFactory that checks for existing
registrations in RuntimeWiring.Builder.

Closes gh-244
  • Loading branch information
rstoyanchev committed Jan 14, 2022
1 parent 2ecac0b commit 5ff3f8b
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}

}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<String, Function<Boolean, DataFetcher<?>>> dataFetcherFactories;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -70,9 +71,9 @@
* {@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
Expand Down Expand Up @@ -150,17 +151,54 @@ public static <T> ReactiveBuilder<T, T> builder(ReactiveQueryByExampleExecutor<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
* registers {@link DataFetcher}s for those queries.
* <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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -77,9 +78,9 @@
* 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
Expand Down Expand Up @@ -173,6 +174,48 @@ public static <T> ReactiveBuilder<T, T> 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.
*
* <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
Expand All @@ -188,8 +231,10 @@ public static <T> ReactiveBuilder<T, T> 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<QuerydslPredicateExecutor<?>> executors,
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
Expand Down Expand Up @@ -291,7 +336,7 @@ public Builder<T, R> sortBy(Sort sort) {
*
* <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
Expand Down Expand Up @@ -394,7 +439,7 @@ public ReactiveBuilder<T, R> sortBy(Sort sort) {
*
* <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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 5ff3f8b

Please sign in to comment.