diff --git a/build.gradle b/build.gradle
index 079cafb2..44d0f1b9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -65,6 +65,7 @@ configure(moduleProjects) {
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.6.0"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2"
mavenBom "org.junit:junit-bom:5.8.1"
+ mavenBom "org.testcontainers:testcontainers-bom:1.16.2"
}
dependencies {
dependency "com.graphql-java:graphql-java:${graphQlJavaVersion}"
@@ -74,6 +75,14 @@ configure(moduleProjects) {
dependency "org.assertj:assertj-core:3.21.0"
dependency "com.jayway.jsonpath:json-path:2.5.0"
dependency "org.skyscreamer:jsonassert:1.5.0"
+ dependency "com.h2database:h2:1.4.200"
+ dependency "org.hibernate:hibernate-core:5.6.1.Final"
+ dependencySet(group: 'org.mongodb', version: '4.3.2') {
+ entry 'mongodb-driver-sync'
+ entry 'mongodb-driver-reactivestreams'
+ entry 'mongodb-driver-core'
+ entry 'bson'
+ }
dependencySet(group: 'org.apache.logging.log4j', version: '2.14.1') {
entry 'log4j-api'
entry 'log4j-core'
diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle
index 00185bea..7fc42d69 100644
--- a/spring-graphql/build.gradle
+++ b/spring-graphql/build.gradle
@@ -7,7 +7,7 @@ dependencies {
api 'io.projectreactor:reactor-core'
api 'org.springframework:spring-context'
- compileOnly 'javax.annotation:javax.annotation-api'
+ compileOnly 'javax.annotation:javax.annotation-api'
compileOnly 'org.springframework:spring-webflux'
compileOnly 'org.springframework:spring-webmvc'
compileOnly 'org.springframework:spring-websocket'
@@ -32,6 +32,14 @@ dependencies {
testImplementation 'org.springframework:spring-test'
testImplementation 'org.springframework.data:spring-data-commons'
testImplementation 'org.springframework.data:spring-data-keyvalue'
+ testImplementation 'org.springframework.data:spring-data-jpa'
+ testImplementation 'com.h2database:h2'
+ testImplementation 'org.hibernate:hibernate-core'
+ testImplementation 'org.springframework.data:spring-data-mongodb'
+ testImplementation 'org.mongodb:mongodb-driver-sync'
+ testImplementation 'org.mongodb:mongodb-driver-reactivestreams'
+ testImplementation 'org.testcontainers:mongodb'
+ testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.springframework.security:spring-security-core'
testImplementation 'com.querydsl:querydsl-core'
testImplementation 'com.querydsl:querydsl-collections'
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentInstantiator.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentInstantiator.java
index eb51f487..df8cbd8e 100644
--- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentInstantiator.java
+++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/GraphQlArgumentInstantiator.java
@@ -39,8 +39,9 @@ import org.springframework.validation.DataBinder;
* {@link graphql.schema.DataFetchingEnvironment} arguments.
*
* @author Brian Clozel
+ * @author Greg Turnquist
*/
-class GraphQlArgumentInstantiator {
+public class GraphQlArgumentInstantiator {
private final DataBinder converter;
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/PropertySelection.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/PropertySelection.java
new file mode 100644
index 00000000..383fa271
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/PropertySelection.java
@@ -0,0 +1,198 @@
+/*
+ * 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
+ *
+ * http://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.querybyexample;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import graphql.schema.DataFetchingFieldSelectionSet;
+import graphql.schema.SelectedField;
+
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.util.TypeInformation;
+
+/**
+ * Utility to compute {@link PropertyPath property paths} from
+ * a {@link DataFetchingFieldSelectionSet field selection} considering an underlying
+ * Java type.
+ *
+ * Property paths are created for each selected field that corresponds with a property
+ * on the underlying type. Nested properties are represented with nested paths
+ * if the nesting can be resolved to a concrete type, otherwise the nested path
+ * is considered to be a composite property without further inspection.
+ *
+ * @author Mark Paluch
+ * @since 1.0.0
+ */
+class PropertySelection {
+
+ private final List propertyPaths;
+
+ private PropertySelection(List propertyPaths) {
+ this.propertyPaths = propertyPaths;
+ }
+
+ /**
+ * Create a property selection for the given {@link TypeInformation type} and
+ * {@link DataFetchingFieldSelectionSet}.
+ *
+ * @param typeInformation the type to inspect
+ * @param selectionSet the field selection to apply
+ * @return a property selection holding all selectable property paths.
+ */
+ public static PropertySelection create(TypeInformation> typeInformation,
+ DataFetchingFieldSelectionSet selectionSet) {
+ return create(typeInformation, new DataFetchingFieldSelection(selectionSet));
+ }
+
+ private static PropertySelection create(TypeInformation> typeInformation, FieldSelection selection) {
+ List propertyPaths = collectPropertyPaths(typeInformation,
+ selection,
+ path -> PropertyPath.from(path, typeInformation));
+ return new PropertySelection(propertyPaths);
+ }
+
+ private static List collectPropertyPaths(TypeInformation> typeInformation,
+ FieldSelection selection, Function propertyPathFactory) {
+ List propertyPaths = new ArrayList<>();
+
+ for (SelectedField selectedField : selection) {
+
+ String propertyName = selectedField.getName();
+ TypeInformation> property = typeInformation.getProperty(propertyName);
+
+ if (property == null) {
+ continue;
+ }
+
+ PropertyPath propertyPath = propertyPathFactory.apply(propertyName);
+ FieldSelection nestedSelection = selection.select(selectedField);
+
+ List pathsToAdd = Collections.singletonList(propertyPath);
+
+ if (!nestedSelection.isEmpty() && property.getActualType() != null) {
+ List nestedPaths = collectPropertyPaths(property.getRequiredActualType(),
+ nestedSelection, propertyPath::nested);
+
+ if (!nestedPaths.isEmpty()) {
+ pathsToAdd = nestedPaths;
+ }
+ }
+
+ propertyPaths.addAll(pathsToAdd);
+ }
+
+ return propertyPaths;
+ }
+
+ /**
+ * @return the property paths as list.
+ */
+ public List toList() {
+ return this.propertyPaths.stream().map(PropertyPath::toDotPath)
+ .collect(Collectors.toList());
+ }
+
+ enum EmptyFieldSelection implements FieldSelection {
+
+ INSTANCE;
+
+ @Override
+ public boolean isEmpty() {
+ return true;
+ }
+
+ @Override
+ public FieldSelection select(SelectedField field) {
+ return INSTANCE;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return Collections.emptyIterator();
+ }
+
+ }
+
+ /**
+ * Hierarchical representation of selected fields. Allows traversing the
+ * object graph with nested fields.
+ */
+ interface FieldSelection extends Iterable {
+
+ /**
+ * @return {@code true} if the field selection is empty
+ */
+ boolean isEmpty();
+
+ /**
+ * Obtain the field selection (nested fields) for a given {@code field}.
+ *
+ * @param field the field for which nested fields should be obtained
+ * @return the field selection. Can be empty.
+ */
+ FieldSelection select(SelectedField field);
+
+ }
+
+ static class DataFetchingFieldSelection implements FieldSelection {
+
+ private final List selectedFields;
+
+ private final List allFields;
+
+ DataFetchingFieldSelection(DataFetchingFieldSelectionSet selectionSet) {
+ this.selectedFields = selectionSet.getImmediateFields();
+ this.allFields = selectionSet.getFields();
+ }
+
+ private DataFetchingFieldSelection(List selectedFields,
+ List allFields) {
+ this.selectedFields = selectedFields;
+ this.allFields = allFields;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return selectedFields.isEmpty();
+ }
+
+ @Override
+ public FieldSelection select(SelectedField field) {
+ List selectedFields = new ArrayList<>();
+
+ for (SelectedField selectedField : allFields) {
+ if (field.equals(selectedField.getParentField())) {
+ selectedFields.add(selectedField);
+ }
+ }
+
+ return (selectedFields.isEmpty() ? EmptyFieldSelection.INSTANCE
+ : new DataFetchingFieldSelection(selectedFields, allFields));
+ }
+
+ @Override
+ public Iterator iterator() {
+ return this.selectedFields.iterator();
+ }
+
+ }
+}
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/QueryByExampleDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/QueryByExampleDataFetcher.java
new file mode 100644
index 00000000..fe397dac
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/QueryByExampleDataFetcher.java
@@ -0,0 +1,662 @@
+/*
+ * 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.data.querybyexample;
+
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import graphql.schema.DataFetcher;
+import graphql.schema.DataFetchingEnvironment;
+import graphql.schema.DataFetchingFieldSelectionSet;
+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;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.Sort;
+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.repository.query.FluentQuery;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
+import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
+import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.graphql.data.GraphQlRepository;
+import org.springframework.graphql.data.method.annotation.support.GraphQlArgumentInstantiator;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Main class to create a {@link DataFetcher} from a Query By Example repository.
+ * To create an instance, use one of the following:
+ *
+ * - {@link #builder(QueryByExampleExecutor)}
+ *
- {@link #builder(ReactiveQueryByExampleExecutor)}
+ *
+ *
+ * For example:
+ *
+ *
+ * interface BookRepository extends
+ * Repository<Book, String>, QueryByExampleExecutor<Book>{}
+ *
+ * TypeRuntimeWiring wiring = … ;
+ * BookRepository repository = … ;
+ *
+ * DataFetcher<?> forMany =
+ * wiring.dataFetcher("books", QueryByExampleDataFetcher.builder(repository).many());
+ *
+ * DataFetcher<?> forSingle =
+ * wiring.dataFetcher("book", QueryByExampleDataFetcher.builder(repository).single());
+ *
+ *
+ * See methods on {@link QueryByExampleDataFetcher.Builder} and {@link QueryByExampleDataFetcher.ReactiveBuilder} for further
+ * options on GraphQL Query argument to Query by Example bindings, result projections, and sorting.
+ *
+ * @param returned result type
+ * @author Greg Turnquist
+ * @see QueryByExampleExecutor
+ * @see ReactiveQueryByExampleExecutor
+ * @see Example
+ * @see
+ * Spring Data Query By Example extension
+ * @since 1.0.0
+ */
+public abstract class QueryByExampleDataFetcher {
+
+ private final TypeInformation domainType;
+
+ private GraphQlArgumentInstantiator instantiator;
+
+ QueryByExampleDataFetcher(TypeInformation domainType, @Nullable ConversionService conversionService) {
+ this.domainType = domainType;
+ this.instantiator = new GraphQlArgumentInstantiator(conversionService);
+ }
+
+ /**
+ * Create a new {@link Builder} accepting {@link QueryByExampleExecutor}
+ * to build a {@link DataFetcher}.
+ *
+ * @param executor the repository object to use
+ * @param result type
+ * @return a new builder
+ */
+ public static Builder builder(QueryByExampleExecutor executor) {
+ return new Builder<>(executor, getDomainType(executor));
+ }
+
+ /**
+ * Create a new {@link ReactiveBuilder} accepting
+ * {@link ReactiveQueryByExampleExecutor} to build a reactive {@link DataFetcher}.
+ *
+ * @param executor the repository object to use
+ * @param result type
+ * @return a new builder
+ */
+ public static ReactiveBuilder builder(ReactiveQueryByExampleExecutor executor) {
+ return new ReactiveBuilder<>(executor, getDomainType(executor));
+ }
+
+ /**
+ * 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.
+ * Note: 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 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")
+ private static Class getDomainType(Object executor) {
+ Class> repositoryInterface = getRepositoryInterface(executor);
+ DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
+ return (Class) metadata.getDomainType();
+ }
+
+ private static Class> getRepositoryInterface(Object executor) {
+ Assert.isInstanceOf(Repository.class, executor);
+
+ Type[] genericInterfaces = executor.getClass().getGenericInterfaces();
+ for (Type genericInterface : genericInterfaces) {
+ Class> rawClass = ResolvableType.forType(genericInterface).getRawClass();
+ if (rawClass == null || MergedAnnotations.from(rawClass).isPresent(NoRepositoryBean.class)) {
+ continue;
+ }
+ if (Repository.class.isAssignableFrom(rawClass)) {
+ return rawClass;
+ }
+ }
+
+ throw new IllegalArgumentException(
+ String.format("Cannot resolve repository interface from %s", executor));
+ }
+
+ /**
+ * Prepare an {@link Example} from GraphQL query arguments.
+ *
+ * @param environment contextual info for the GraphQL query
+ * @return the resulting example
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ protected Example buildExample(DataFetchingEnvironment environment) {
+ return Example.of(instantiator.instantiate(environment.getArguments(), domainType.getType()));
+ }
+
+ protected boolean requiresProjection(Class> resultType) {
+ return !resultType.equals(this.domainType.getType());
+ }
+
+ protected Collection buildPropertyPaths(DataFetchingFieldSelectionSet selection, Class> resultType) {
+
+ // Compute selection only for non-projections
+ if (this.domainType.getType().equals(resultType) ||
+ this.domainType.getType().isAssignableFrom(resultType) ||
+ this.domainType.isSubTypeOf(resultType)) {
+ return PropertySelection.create(this.domainType, selection).toList();
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Builder for a Query by Example-based {@link DataFetcher}. Note that builder
+ * instances are immutable and return a new instance of the builder
+ * when calling configuration methods.
+ *
+ * @param domain type
+ * @param result type
+ */
+ public static class Builder {
+
+ private final QueryByExampleExecutor executor;
+
+ private final ClassTypeInformation domainType;
+
+ private final Class resultType;
+
+ private final Sort sort;
+
+ private final ConversionService conversionService;
+
+ @SuppressWarnings("unchecked")
+ Builder(QueryByExampleExecutor executor, Class domainType) {
+ this(executor,
+ ClassTypeInformation.from((Class) domainType),
+ domainType,
+ Sort.unsorted(),
+ null);
+ }
+
+ Builder(QueryByExampleExecutor executor, ClassTypeInformation domainType, Class resultType, Sort sort, ConversionService conversionService) {
+ this.executor = executor;
+ this.domainType = domainType;
+ this.resultType = resultType;
+ this.sort = sort;
+ this.conversionService = conversionService;
+ }
+
+ /**
+ * Project results returned from the {@link QueryByExampleExecutor}
+ * into the target {@code projectionType}. Projection types can be
+ * either interfaces declaring getters for properties to expose or
+ * regular classes outside the entity type hierarchy for
+ * DTO projection.
+ *
+ * @param projectionType projection type
+ * @return a new {@link Builder} instance with all previously
+ * configured options and {@code projectionType} applied
+ */
+ public Builder projectAs(Class projectionType) {
+ Assert.notNull(projectionType, "Projection type must not be null");
+ return new Builder<>(this.executor, this.domainType, projectionType, this.sort, this.conversionService);
+ }
+
+ /**
+ * Apply a {@link Sort} order.
+ *
+ * @param sort the default sort order
+ * @return a new {@link Builder} instance with all previously configured
+ * options and {@code Sort} applied
+ */
+ public Builder sortBy(Sort sort) {
+ Assert.notNull(sort, "Sort must not be null");
+ return new Builder<>(this.executor, this.domainType, this.resultType, sort, this.conversionService);
+ }
+
+ /**
+ * Build a {@link DataFetcher} to fetch single object instances.
+ *
+ * @return a {@link DataFetcher} based on Query by Example to fetch one object
+ */
+ public DataFetcher single() {
+ return new SingleEntityFetcher<>(
+ this.executor, this.domainType, this.resultType, this.sort, this.conversionService);
+ }
+
+ /**
+ * Build a {@link DataFetcher} to fetch many object instances.
+ *
+ * @return a {@link DataFetcher} based on Query Example to fetch many objects
+ */
+ public DataFetcher> many() {
+ return new ManyEntityFetcher<>(
+ this.executor, this.domainType, this.resultType, this.sort, this.conversionService);
+ }
+ }
+
+ /**
+ * Builder for a reactive Query by Example-based {@link DataFetcher}. Note that builder
+ * instances are immutable and return a new instance of the builder when
+ * calling configuration methods.
+ *
+ * @param domain type
+ * @param result type
+ */
+ public static class ReactiveBuilder {
+
+ private final ReactiveQueryByExampleExecutor executor;
+
+ private final TypeInformation domainType;
+
+ private final Class resultType;
+
+ private final Sort sort;
+
+ private final ConversionService conversionService;
+
+ @SuppressWarnings("unchecked")
+ ReactiveBuilder(ReactiveQueryByExampleExecutor executor, Class domainType) {
+ this(executor,
+ ClassTypeInformation.from((Class) domainType),
+ domainType,
+ Sort.unsorted(),
+ null);
+ }
+
+ ReactiveBuilder(ReactiveQueryByExampleExecutor executor,
+ TypeInformation domainType,
+ Class resultType,
+ Sort sort,
+ ConversionService conversionService) {
+ this.executor = executor;
+ this.domainType = domainType;
+ this.resultType = resultType;
+ this.sort = sort;
+ this.conversionService = conversionService;
+ }
+
+ /**
+ * Project results returned from the {@link QueryByExampleExecutor}
+ * into the target {@code projectionType}. Projection types can be
+ * either interfaces declaring getters for properties to expose or
+ * regular classes outside the entity type hierarchy for
+ * DTO projection.
+ *
+ * @param projectionType projection type
+ * @return a new {@link ReactiveBuilder} instance with all previously
+ * configured options and {@code projectionType} applied
+ */
+ public ReactiveBuilder projectAs(Class projectionType) {
+ Assert.notNull(projectionType, "Projection type must not be null");
+ return new ReactiveBuilder<>(this.executor, this.domainType, projectionType, this.sort, this.conversionService);
+ }
+
+ /**
+ * Apply a {@link Sort} order.
+ *
+ * @param sort the default sort order
+ * @return a new {@link ReactiveBuilder} instance with all previously configured
+ * options and {@code Sort} applied
+ */
+ public ReactiveBuilder sortBy(Sort sort) {
+ Assert.notNull(sort, "Sort must not be null");
+ return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType, sort, this.conversionService);
+ }
+
+ /**
+ * Build a {@link DataFetcher} to fetch single object instances through {@link Mono}.
+ *
+ * @return a {@link DataFetcher} based on Query by Example to fetch one object
+ */
+ public DataFetcher> single() {
+ return new ReactiveSingleEntityFetcher<>(
+ this.executor, this.domainType, this.resultType, this.sort, this.conversionService);
+ }
+
+ /**
+ * Build a {@link DataFetcher} to fetch many object instances through {@link Flux}.
+ *
+ * @return a {@link DataFetcher} based on Query by Example to fetch many objects
+ */
+ public DataFetcher> many() {
+ return new ReactiveManyEntityFetcher<>(
+ this.executor, this.domainType, this.resultType, this.sort, this.conversionService);
+ }
+ }
+
+ private static class SingleEntityFetcher extends QueryByExampleDataFetcher implements DataFetcher {
+
+ private final QueryByExampleExecutor executor;
+
+ private final Class resultType;
+
+ private final Sort sort;
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ SingleEntityFetcher(QueryByExampleExecutor executor,
+ TypeInformation domainType,
+ Class resultType,
+ Sort sort,
+ ConversionService conversionService) {
+
+ super(domainType, conversionService);
+ this.executor = executor;
+ this.resultType = resultType;
+ this.sort = sort;
+ }
+
+ @Override
+ @SuppressWarnings({"ConstantConditions", "unchecked"})
+ public R get(DataFetchingEnvironment env) {
+ return this.executor.findBy(buildExample(env), query -> {
+ FluentQuery.FetchableFluentQuery queryToUse = (FluentQuery.FetchableFluentQuery) query;
+
+ if (this.sort.isSorted()) {
+ queryToUse = queryToUse.sortBy(this.sort);
+ }
+
+ Class resultType = this.resultType;
+ if (requiresProjection(resultType)) {
+ queryToUse = queryToUse.as(resultType);
+ }
+ else {
+ queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), resultType));
+ }
+
+ return queryToUse.first();
+ }).orElse(null);
+ }
+
+ }
+
+ private static class ManyEntityFetcher extends QueryByExampleDataFetcher implements DataFetcher> {
+
+ private final QueryByExampleExecutor executor;
+
+ private final Class resultType;
+
+ private final Sort sort;
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ ManyEntityFetcher(QueryByExampleExecutor executor,
+ TypeInformation domainType,
+ Class resultType,
+ Sort sort,
+ ConversionService conversionService) {
+ super(domainType, conversionService);
+ this.executor = executor;
+ this.resultType = resultType;
+ this.sort = sort;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Iterable get(DataFetchingEnvironment env) {
+ return this.executor.findBy(buildExample(env), query -> {
+ FluentQuery.FetchableFluentQuery queryToUse = (FluentQuery.FetchableFluentQuery) query;
+
+ if (this.sort.isSorted()) {
+ queryToUse = queryToUse.sortBy(this.sort);
+ }
+
+ if (requiresProjection(this.resultType)) {
+ queryToUse = queryToUse.as(this.resultType);
+ }
+ else {
+ queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
+ }
+
+ return queryToUse.all();
+ });
+ }
+
+ }
+
+ private static class ReactiveSingleEntityFetcher extends QueryByExampleDataFetcher implements DataFetcher> {
+
+ private final ReactiveQueryByExampleExecutor executor;
+
+ private final Class resultType;
+
+ private final Sort sort;
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ ReactiveSingleEntityFetcher(ReactiveQueryByExampleExecutor executor,
+ TypeInformation domainType,
+ Class resultType,
+ Sort sort,
+ ConversionService conversionService) {
+
+ super(domainType, conversionService);
+ this.executor = executor;
+ this.resultType = resultType;
+ this.sort = sort;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Mono get(DataFetchingEnvironment env) {
+ return this.executor.findBy(buildExample(env), query -> {
+ FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) query;
+
+ if (this.sort.isSorted()) {
+ queryToUse = queryToUse.sortBy(this.sort);
+ }
+
+ if (requiresProjection(this.resultType)) {
+ queryToUse = queryToUse.as(this.resultType);
+ }
+ else {
+ queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
+ }
+
+ return queryToUse.first();
+ });
+ }
+
+ }
+
+ private static class ReactiveManyEntityFetcher extends QueryByExampleDataFetcher implements DataFetcher> {
+
+ private final ReactiveQueryByExampleExecutor executor;
+
+ private final Class resultType;
+
+ private final Sort sort;
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ ReactiveManyEntityFetcher(ReactiveQueryByExampleExecutor executor,
+ TypeInformation domainType,
+ Class resultType,
+ Sort sort,
+ ConversionService conversionService) {
+
+ super(domainType, conversionService);
+ this.executor = executor;
+ this.resultType = resultType;
+ this.sort = sort;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Flux get(DataFetchingEnvironment env) {
+ return this.executor.findBy(buildExample(env), query -> {
+ FluentQuery.ReactiveFluentQuery queryToUse = (FluentQuery.ReactiveFluentQuery) query;
+
+ if (this.sort.isSorted()) {
+ queryToUse = queryToUse.sortBy(this.sort);
+ }
+
+ if (requiresProjection(this.resultType)) {
+ queryToUse = queryToUse.as(this.resultType);
+ }
+ else {
+ queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
+ }
+
+ return queryToUse.all();
+ });
+ }
+ }
+
+ /**
+ * GraphQLTypeVisitor that auto-registers Query By Example Spring Data 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) {
+
+ Map>> map = new HashMap<>();
+
+ for (QueryByExampleExecutor> executor : executors) {
+ String typeName = getTypeName(executor);
+ if (typeName != null) {
+ map.put(typeName, (single) -> single ?
+ builder(executor).single() :
+ builder(executor).many());
+ }
+ }
+
+ for (ReactiveQueryByExampleExecutor> reactiveExecutor : reactiveExecutors) {
+ String typeName = getTypeName(reactiveExecutor);
+ if (typeName != null) {
+ map.put(typeName, (single) -> single ?
+ builder(reactiveExecutor).single() :
+ builder(reactiveExecutor).many());
+ }
+ }
+
+ return map;
+ }
+
+ @Nullable
+ private String getTypeName(Object repository) {
+ GraphQlRepository annotation =
+ AnnotatedElementUtils.findMergedAnnotation(repository.getClass(), GraphQlRepository.class);
+
+ if (annotation == null) {
+ return null;
+ }
+ if (StringUtils.hasText(annotation.typeName())) {
+ return annotation.typeName();
+ }
+ Class> repositoryInterface = getRepositoryInterface(repository);
+ RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
+ return metadata.getDomainType().getSimpleName();
+ }
+
+ @Override
+ public TraversalControl visitGraphQLFieldDefinition(
+ GraphQLFieldDefinition fieldDefinition, TraverserContext context) {
+
+ if (this.executorMap.isEmpty()) {
+ return TraversalControl.QUIT;
+ }
+
+ GraphQLType fieldType = fieldDefinition.getType();
+ GraphQLFieldsContainer parent = (GraphQLFieldsContainer) context.getParentNode();
+ if (!parent.getName().equals("Query")) {
+ return TraversalControl.ABORT;
+ }
+
+ DataFetcher> dataFetcher = (fieldType instanceof GraphQLList ?
+ getDataFetcher(((GraphQLList) fieldType).getWrappedType(), false) :
+ getDataFetcher(fieldType, true));
+
+ if (dataFetcher != null) {
+ 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/data/querybyexample/package-info.java b/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/package-info.java
new file mode 100644
index 00000000..6b1a7652
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/data/querybyexample/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+/**
+ * Support for {@link graphql.schema.DataFetcher}s backed by Querydsl based
+ * Spring Data repositories.
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.graphql.data.querybyexample;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/Author.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/Author.java
new file mode 100644
index 00000000..c6aa372a
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/Author.java
@@ -0,0 +1,67 @@
+/*
+ * 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.data.querybyexample.jpa;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+@Entity
+public class Author {
+
+ @Id
+ Long id;
+
+ String firstName;
+
+ String lastName;
+
+ public Author() {
+ }
+
+ public Author(Long id, String firstName, String lastName) {
+ this.id = id;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getFirstName() {
+ return this.firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return this.lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getFullName() {
+ return this.firstName + " " + this.lastName;
+ }
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/Book.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/Book.java
new file mode 100644
index 00000000..8592e8b4
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/Book.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+package org.springframework.graphql.data.querybyexample.jpa;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.OneToOne;
+
+@Entity
+public class Book {
+
+ @Id
+ Long id;
+
+ String name;
+
+ @OneToOne(cascade = CascadeType.ALL)
+ Author author = new Author();
+
+ public Book() {
+ }
+
+ public Book(Long id, String name, Author author) {
+ this.id = id;
+ this.name = name;
+ this.author = author;
+ }
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Author getAuthor() {
+ return this.author;
+ }
+
+ public void setAuthor(Author author) {
+ this.author = author;
+ }
+
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/BookRepository.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/BookRepository.java
new file mode 100644
index 00000000..62d8cc7b
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/BookRepository.java
@@ -0,0 +1,8 @@
+package org.springframework.graphql.data.querybyexample.jpa;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.graphql.data.GraphQlRepository;
+
+@GraphQlRepository
+public interface BookRepository extends JpaRepository {
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/QueryByExampleDataFetcherJpaTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/QueryByExampleDataFetcherJpaTests.java
new file mode 100644
index 00000000..22ec804e
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/jpa/QueryByExampleDataFetcherJpaTests.java
@@ -0,0 +1,249 @@
+/*
+ * 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
+ *
+ * http://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.querybyexample.jpa;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+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;
+import reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
+import org.springframework.graphql.BookSource;
+import org.springframework.graphql.GraphQlResponse;
+import org.springframework.graphql.GraphQlSetup;
+import org.springframework.graphql.data.querybyexample.QueryByExampleDataFetcher;
+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.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.lang.Nullable;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link QueryByExampleDataFetcher}.
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration
+class QueryByExampleDataFetcherJpaTests {
+
+ @Autowired
+ private BookRepository repository;
+
+ static GraphQlSetup graphQlSetup(String fieldName, DataFetcher> fetcher) {
+ return initGraphQlSetup(null).queryFetcher(fieldName, fetcher);
+ }
+
+ static GraphQlSetup graphQlSetup(@Nullable QueryByExampleExecutor> executor) {
+ return initGraphQlSetup(executor);
+ }
+
+ private static GraphQlSetup initGraphQlSetup(
+ @Nullable QueryByExampleExecutor> executor) {
+
+ GraphQLTypeVisitor visitor = QueryByExampleDataFetcher.registrationTypeVisitor(
+ executor != null
+ ? Collections.singletonList(executor)
+ : Collections.emptyList(),
+ Collections.emptyList());
+
+ return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor);
+ }
+
+ @Test
+ void shouldFetchSingleItems() {
+ Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
+ repository.save(book);
+
+ Consumer tester = setup -> {
+ Mono output = setup.toWebGraphQlHandler().handleRequest(input("{ bookById(id: 42) {name}}"));
+ Book actualBook = GraphQlResponse.from(output).toEntity("bookById", Book.class);
+
+ assertThat(actualBook.getName()).isEqualTo(book.getName());
+ };
+
+ // explicit wiring
+ tester.accept(graphQlSetup("bookById", QueryByExampleDataFetcher.builder(repository).single()));
+
+ // auto registration
+ tester.accept(graphQlSetup(repository));
+ }
+
+ @Test
+ void shouldFetchMultipleItems() {
+ Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
+ Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"));
+ repository.saveAll(Arrays.asList(book1, book2));
+
+ Consumer tester = graphQlSetup -> {
+ Mono output = graphQlSetup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
+
+ List names = GraphQlResponse.from(output).toList("books", Book.class)
+ .stream()
+ .map(Book::getName)
+ .collect(Collectors.toList());
+
+ assertThat(names).containsExactlyInAnyOrder(book1.getName(), book2.getName());
+ };
+
+ // explicit wiring
+ tester.accept(graphQlSetup("books", QueryByExampleDataFetcher.builder(repository).many()));
+
+ // auto registration
+ tester.accept(graphQlSetup(repository));
+ }
+
+ @Test
+ void shouldFavorExplicitWiring() {
+ BookRepository mockRepository = mock(BookRepository.class);
+ Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
+ when(mockRepository.findBy(any(), any())).thenReturn(Optional.of(book));
+
+ // 1) Automatic registration only
+ WebGraphQlHandler handler = graphQlSetup(mockRepository).toWebGraphQlHandler();
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy");
+
+ // 2) Automatic registration and explicit wiring
+ handler = graphQlSetup(mockRepository)
+ .queryFetcher("bookById", env -> new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg")))
+ .toWebGraphQlHandler();
+
+ outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
+
+ actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("Breaking Bad");
+ }
+
+ @Test
+ void shouldFetchSingleItemsWithInterfaceProjection() {
+ Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
+ repository.save(book);
+
+ DataFetcher> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookProjection.class).single();
+ WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
+
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy by Douglas Adams");
+ }
+
+ @Disabled("Pending https://github.com/spring-projects/spring-data-jpa/issues/2327")
+ @Test
+ void shouldFetchSingleItemsWithDtoProjection() {
+ Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
+ repository.save(book);
+
+ DataFetcher> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookDto.class).single();
+ WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
+
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
+ }
+
+ private WebInput input(String query) {
+ return new WebInput(URI.create("/"), new HttpHeaders(), Collections.singletonMap("query", query), null, "1");
+ }
+
+
+ interface BookProjection {
+
+ @Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
+ String getName();
+
+ }
+
+ static class BookDto {
+
+ private final String name;
+
+ public BookDto(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return "The book is: " + name;
+ }
+
+ }
+
+ @Configuration
+ @EnableJpaRepositories(considerNestedRepositories = true)
+ static class TestConfig {
+
+ @Bean
+ DriverManagerDataSource dataSource() {
+ DriverManagerDataSource dataSource = new DriverManagerDataSource();
+ dataSource.setDriverClassName("org.h2.Driver");
+ dataSource.setUrl("jdbc:h2:mem:query-by-example-test;DB_CLOSE_DELAY=-1");
+ return dataSource;
+ }
+
+ @Bean
+ LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
+ HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
+ jpaVendorAdapter.setGenerateDdl(true);
+ jpaVendorAdapter.setShowSql(true);
+
+ LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
+ factory.setDataSource(dataSource);
+ factory.setJpaVendorAdapter(jpaVendorAdapter);
+ factory.setPackagesToScan(QueryByExampleDataFetcherJpaTests.class.getPackage().getName());
+ return factory;
+ }
+
+ @Bean
+ PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
+ JpaTransactionManager transactionManager = new JpaTransactionManager();
+ transactionManager.setEntityManagerFactory(entityManagerFactory);
+ return transactionManager;
+ }
+ }
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/Author.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/Author.java
new file mode 100644
index 00000000..fb53cf7c
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/Author.java
@@ -0,0 +1,65 @@
+/*
+ * 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.data.querybyexample.mongodb;
+
+import org.springframework.data.annotation.Id;
+
+public class Author {
+
+ @Id
+ String id;
+
+ String firstName;
+
+ String lastName;
+
+ public Author() {
+ }
+
+ public Author(String id, String firstName, String lastName) {
+ this.id = id;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getFirstName() {
+ return this.firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return this.lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getFullName() {
+ return this.firstName + " " + this.lastName;
+ }
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/Book.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/Book.java
new file mode 100644
index 00000000..92351380
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/Book.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+package org.springframework.graphql.data.querybyexample.mongodb;
+
+import org.springframework.data.annotation.Id;
+
+public class Book {
+
+ @Id
+ String id;
+
+ String name;
+
+ Author author = new Author();
+
+ public Book() {
+ }
+
+ public Book(String id, String name, Author author) {
+ this.id = id;
+ this.name = name;
+ 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 Author getAuthor() {
+ return this.author;
+ }
+
+ public void setAuthor(Author author) {
+ this.author = author;
+ }
+
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/BookRepository.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/BookRepository.java
new file mode 100644
index 00000000..3e731fc4
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/BookRepository.java
@@ -0,0 +1,8 @@
+package org.springframework.graphql.data.querybyexample.mongodb;
+
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.graphql.data.GraphQlRepository;
+
+@GraphQlRepository
+public interface BookRepository extends MongoRepository {
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/QueryByExampleDataFetcherMongoDbTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/QueryByExampleDataFetcherMongoDbTests.java
new file mode 100644
index 00000000..b6a9e7f8
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/QueryByExampleDataFetcherMongoDbTests.java
@@ -0,0 +1,222 @@
+/*
+ * 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
+ *
+ * http://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.querybyexample.mongodb;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+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;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+import reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
+import org.springframework.graphql.BookSource;
+import org.springframework.graphql.GraphQlResponse;
+import org.springframework.graphql.GraphQlSetup;
+import org.springframework.graphql.data.querybyexample.QueryByExampleDataFetcher;
+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 org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+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.when;
+
+/**
+ * Unit tests for {@link QueryByExampleDataFetcher}.
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration
+@Testcontainers
+class QueryByExampleDataFetcherMongoDbTests {
+
+ @Container
+ static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
+
+ @Autowired
+ private BookRepository repository;
+
+ static GraphQlSetup graphQlSetup(String fieldName, DataFetcher> fetcher) {
+ return initGraphQlSetup(null).queryFetcher(fieldName, fetcher);
+ }
+
+ static GraphQlSetup graphQlSetup(@Nullable QueryByExampleExecutor> executor) {
+ return initGraphQlSetup(executor);
+ }
+
+ private static GraphQlSetup initGraphQlSetup(
+ @Nullable QueryByExampleExecutor> executor) {
+
+ GraphQLTypeVisitor visitor = QueryByExampleDataFetcher.registrationTypeVisitor(
+ (executor != null ? Collections.singletonList(executor) : Collections.emptyList()),
+ Collections.emptyList());
+
+ return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor);
+ }
+
+ @Test
+ void shouldFetchSingleItems() {
+ Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ repository.save(book);
+
+ Consumer tester = setup -> {
+ Mono output = setup.toWebGraphQlHandler().handleRequest(input("{ bookById(id: 42) {name}}"));
+ Book actualBook = GraphQlResponse.from(output).toEntity("bookById", Book.class);
+
+ assertThat(actualBook.getName()).isEqualTo(book.getName());
+ };
+
+ // explicit wiring
+ tester.accept(graphQlSetup("bookById", QueryByExampleDataFetcher.builder(repository).single()));
+
+ // auto registration
+ tester.accept(graphQlSetup(repository));
+ }
+
+ @Test
+ void shouldFetchMultipleItems() {
+ Book book1 = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ Book book2 = new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg"));
+ repository.saveAll(Arrays.asList(book1, book2));
+
+ Consumer tester = graphQlSetup -> {
+ Mono output = graphQlSetup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
+
+ List names = GraphQlResponse.from(output).toList("books", Book.class)
+ .stream().map(Book::getName).collect(Collectors.toList());
+
+ assertThat(names).containsExactlyInAnyOrder(book1.getName(), book2.getName());
+ };
+
+ // explicit wiring
+ tester.accept(graphQlSetup("books", QueryByExampleDataFetcher.builder(repository).many()));
+
+ // auto registration
+ tester.accept(graphQlSetup(repository));
+ }
+
+ @Test
+ void shouldFavorExplicitWiring() {
+ BookRepository mockRepository = mock(BookRepository.class);
+ Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ when(mockRepository.findBy(any(), any())).thenReturn(Optional.of(book));
+
+ // 1) Automatic registration only
+ WebGraphQlHandler handler = graphQlSetup(mockRepository).toWebGraphQlHandler();
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy");
+
+ // 2) Automatic registration and explicit wiring
+ handler = graphQlSetup(mockRepository)
+ .queryFetcher("bookById", env -> new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg")))
+ .toWebGraphQlHandler();
+
+ outputMono = handler.handleRequest(input("{ bookById(id: 1) {name}}"));
+
+ actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("Breaking Bad");
+ }
+
+ @Test
+ void shouldFetchSingleItemsWithInterfaceProjection() {
+ Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ repository.save(book);
+
+ DataFetcher> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookProjection.class).single();
+ WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
+
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy by Douglas Adams");
+ }
+
+ @Test
+ void shouldFetchSingleItemsWithDtoProjection() {
+ Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ repository.save(book);
+
+ DataFetcher> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookDto.class).single();
+ WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
+
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
+ }
+
+ private WebInput input(String query) {
+ return new WebInput(URI.create("/"), new HttpHeaders(), Collections.singletonMap("query", query), null, "1");
+ }
+
+ interface BookProjection {
+
+ @Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
+ String getName();
+ }
+
+ static class BookDto {
+
+ private final String name;
+
+ public BookDto(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return "The book is: " + name;
+ }
+ }
+
+ @Configuration
+ @EnableMongoRepositories(considerNestedRepositories = true)
+ static class TestConfig {
+
+ @Bean
+ MongoTemplate mongoTemplate() {
+ return new MongoTemplate(MongoClients.create(String.format("mongodb://%s:%d",
+ mongoDBContainer.getContainerIpAddress(),
+ mongoDBContainer.getFirstMappedPort())),
+ "test");
+ }
+ }
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/QueryByExampleDataFetcherReactiveMongoDbTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/QueryByExampleDataFetcherReactiveMongoDbTests.java
new file mode 100644
index 00000000..798a8325
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/QueryByExampleDataFetcherReactiveMongoDbTests.java
@@ -0,0 +1,194 @@
+/*
+ * 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
+ *
+ * http://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.querybyexample.mongodb;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+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;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
+import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
+import org.springframework.graphql.BookSource;
+import org.springframework.graphql.GraphQlResponse;
+import org.springframework.graphql.GraphQlSetup;
+import org.springframework.graphql.data.querybyexample.QueryByExampleDataFetcher;
+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 org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link QueryByExampleDataFetcher}.
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration
+@Testcontainers
+class QueryByExampleDataFetcherReactiveMongoDbTests {
+
+ @Container
+ static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
+
+ @Autowired
+ private ReactiveBookRepository repository;
+
+ static GraphQlSetup graphQlSetup(String fieldName, DataFetcher> fetcher) {
+ return initGraphQlSetup(null).queryFetcher(fieldName, fetcher);
+ }
+
+ static GraphQlSetup graphQlSetup(@Nullable ReactiveQueryByExampleExecutor> executor) {
+ return initGraphQlSetup(executor);
+ }
+
+ private static GraphQlSetup initGraphQlSetup(
+ @Nullable ReactiveQueryByExampleExecutor> reactiveExecutor) {
+
+ GraphQLTypeVisitor visitor = QueryByExampleDataFetcher.registrationTypeVisitor(
+ Collections.emptyList(),
+ (reactiveExecutor != null ? Collections.singletonList(reactiveExecutor) : Collections.emptyList()));
+
+ return GraphQlSetup.schemaResource(BookSource.schema).typeVisitor(visitor);
+ }
+
+ @Test
+ void shouldReactivelyFetchSingleItems() {
+ Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ repository.save(book).block();
+
+ Consumer tester = setup -> {
+ Mono outputMono = setup.toWebGraphQlHandler().handleRequest(input("{ bookById(id: 42) {name}}"));
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+
+ assertThat(actualBook.getName()).isEqualTo(book.getName());
+ };
+
+ // explicit wiring
+ tester.accept(graphQlSetup("bookById", QueryByExampleDataFetcher.builder(repository).single()));
+
+ // auto registration
+ tester.accept(graphQlSetup(repository));
+ }
+
+ @Test
+ void shouldFetchSingleItemsReactivelyWithInterfaceProjection() {
+ Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ repository.save(book).block();
+
+ DataFetcher> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookProjection.class).single();
+ WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
+
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("Hitchhiker's Guide to the Galaxy by Douglas Adams");
+ }
+
+ @Test
+ void shouldFetchSingleItemsReactivelyWithDtoProjection() {
+ Book book = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ repository.save(book).block();
+
+ DataFetcher> fetcher = QueryByExampleDataFetcher.builder(repository).projectAs(BookDto.class).single();
+ WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
+
+ Mono outputMono = handler.handleRequest(input("{ bookById(id: 42) {name}}"));
+
+ Book actualBook = GraphQlResponse.from(outputMono).toEntity("bookById", Book.class);
+ assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
+ }
+
+ @Test
+ void shouldReactivelyFetchMultipleItems() {
+ Book book1 = new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams"));
+ Book book2 = new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg"));
+ repository.saveAll(Flux.just(book1, book2)).blockLast();
+
+ Consumer tester = setup -> {
+ Mono outputMono = setup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
+
+ List names = GraphQlResponse.from(outputMono).toList("books", Book.class)
+ .stream().map(Book::getName).collect(Collectors.toList());
+
+ assertThat(names).containsExactlyInAnyOrder("Breaking Bad", "Hitchhiker's Guide to the Galaxy");
+ };
+
+ // explicit wiring
+ tester.accept(graphQlSetup("books", QueryByExampleDataFetcher.builder(repository).many()));
+
+ // auto registration
+ tester.accept(graphQlSetup(repository));
+ }
+
+ private WebInput input(String query) {
+ return new WebInput(URI.create("/"), new HttpHeaders(), Collections.singletonMap("query", query), null, "1");
+ }
+
+ interface BookProjection {
+
+ @Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
+ String getName();
+ }
+
+ static class BookDto {
+
+ private final String name;
+
+ public BookDto(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return "The book is: " + name;
+ }
+ }
+
+ @Configuration
+ @EnableReactiveMongoRepositories(considerNestedRepositories = true)
+ static class TestConfig {
+
+ @Bean
+ ReactiveMongoTemplate reactiveMongoTemplate() {
+ return new ReactiveMongoTemplate(MongoClients.create(String.format("mongodb://%s:%d",
+ mongoDBContainer.getContainerIpAddress(),
+ mongoDBContainer.getFirstMappedPort())),
+ "test");
+ }
+ }
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/ReactiveBookRepository.java b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/ReactiveBookRepository.java
new file mode 100644
index 00000000..e901e741
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/querybyexample/mongodb/ReactiveBookRepository.java
@@ -0,0 +1,8 @@
+package org.springframework.graphql.data.querybyexample.mongodb;
+
+import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
+import org.springframework.graphql.data.GraphQlRepository;
+
+@GraphQlRepository
+public interface ReactiveBookRepository extends ReactiveMongoRepository {
+}