Support for Query By Example based DataFetcher
This adds support for Spring Data's recent fluent query API via Query by Example. It tests against Spring Data JPA and Spring Data MongoDB for imperative, and Spring Data MongoDB for reactive support. To support MongoDB testing, it uses Testcontainers. See gh-191
This commit is contained in:
committed by
Rossen Stoyanchev
parent
b6897168cf
commit
c9b5652833
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<PropertyPath> propertyPaths;
|
||||
|
||||
private PropertySelection(List<PropertyPath> 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<PropertyPath> propertyPaths = collectPropertyPaths(typeInformation,
|
||||
selection,
|
||||
path -> PropertyPath.from(path, typeInformation));
|
||||
return new PropertySelection(propertyPaths);
|
||||
}
|
||||
|
||||
private static List<PropertyPath> collectPropertyPaths(TypeInformation<?> typeInformation,
|
||||
FieldSelection selection, Function<String, PropertyPath> propertyPathFactory) {
|
||||
List<PropertyPath> 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<PropertyPath> pathsToAdd = Collections.singletonList(propertyPath);
|
||||
|
||||
if (!nestedSelection.isEmpty() && property.getActualType() != null) {
|
||||
List<PropertyPath> 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<String> 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<SelectedField> iterator() {
|
||||
return Collections.emptyIterator();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hierarchical representation of selected fields. Allows traversing the
|
||||
* object graph with nested fields.
|
||||
*/
|
||||
interface FieldSelection extends Iterable<SelectedField> {
|
||||
|
||||
/**
|
||||
* @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<SelectedField> selectedFields;
|
||||
|
||||
private final List<SelectedField> allFields;
|
||||
|
||||
DataFetchingFieldSelection(DataFetchingFieldSelectionSet selectionSet) {
|
||||
this.selectedFields = selectionSet.getImmediateFields();
|
||||
this.allFields = selectionSet.getFields();
|
||||
}
|
||||
|
||||
private DataFetchingFieldSelection(List<SelectedField> selectedFields,
|
||||
List<SelectedField> allFields) {
|
||||
this.selectedFields = selectedFields;
|
||||
this.allFields = allFields;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return selectedFields.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldSelection select(SelectedField field) {
|
||||
List<SelectedField> 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<SelectedField> iterator() {
|
||||
return this.selectedFields.iterator();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>{@link #builder(QueryByExampleExecutor)}
|
||||
* <li>{@link #builder(ReactiveQueryByExampleExecutor)}
|
||||
* </ul>
|
||||
*
|
||||
* <p>For example:
|
||||
*
|
||||
* <pre class="code">
|
||||
* 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());
|
||||
* </pre>
|
||||
*
|
||||
* <p>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 <T> returned result type
|
||||
* @author Greg Turnquist
|
||||
* @see QueryByExampleExecutor
|
||||
* @see ReactiveQueryByExampleExecutor
|
||||
* @see Example
|
||||
* @see <a href="https://docs.spring.io/spring-data/commons/docs/current/reference/html/#query-by-example">
|
||||
* Spring Data Query By Example extension</a>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public abstract class QueryByExampleDataFetcher<T> {
|
||||
|
||||
private final TypeInformation<T> domainType;
|
||||
|
||||
private GraphQlArgumentInstantiator instantiator;
|
||||
|
||||
QueryByExampleDataFetcher(TypeInformation<T> 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 <T> result type
|
||||
* @return a new builder
|
||||
*/
|
||||
public static <T> Builder<T, T> builder(QueryByExampleExecutor<T> 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 <T> result type
|
||||
* @return a new builder
|
||||
*/
|
||||
public static <T> ReactiveBuilder<T, T> builder(ReactiveQueryByExampleExecutor<T> 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.
|
||||
* <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 reactiveExecutors reactive repositories to consider for registration
|
||||
* @return the created visitor
|
||||
*/
|
||||
public static GraphQLTypeVisitor registrationTypeVisitor(
|
||||
List<QueryByExampleExecutor<?>> executors,
|
||||
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
|
||||
|
||||
return new RegistrationTypeVisitor(executors, reactiveExecutors);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> Class<T> getDomainType(Object executor) {
|
||||
Class<?> repositoryInterface = getRepositoryInterface(executor);
|
||||
DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
|
||||
return (Class<T>) 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<T> 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<String> 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 <T> domain type
|
||||
* @param <R> result type
|
||||
*/
|
||||
public static class Builder<T, R> {
|
||||
|
||||
private final QueryByExampleExecutor<T> executor;
|
||||
|
||||
private final ClassTypeInformation<T> domainType;
|
||||
|
||||
private final Class<R> resultType;
|
||||
|
||||
private final Sort sort;
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Builder(QueryByExampleExecutor<T> executor, Class<R> domainType) {
|
||||
this(executor,
|
||||
ClassTypeInformation.from((Class<T>) domainType),
|
||||
domainType,
|
||||
Sort.unsorted(),
|
||||
null);
|
||||
}
|
||||
|
||||
Builder(QueryByExampleExecutor<T> executor, ClassTypeInformation<T> domainType, Class<R> 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 <P> Builder<T, P> projectAs(Class<P> 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<T, R> 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<R> 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<Iterable<R>> 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 <T> domain type
|
||||
* @param <R> result type
|
||||
*/
|
||||
public static class ReactiveBuilder<T, R> {
|
||||
|
||||
private final ReactiveQueryByExampleExecutor<T> executor;
|
||||
|
||||
private final TypeInformation<T> domainType;
|
||||
|
||||
private final Class<R> resultType;
|
||||
|
||||
private final Sort sort;
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ReactiveBuilder(ReactiveQueryByExampleExecutor<T> executor, Class<R> domainType) {
|
||||
this(executor,
|
||||
ClassTypeInformation.from((Class<T>) domainType),
|
||||
domainType,
|
||||
Sort.unsorted(),
|
||||
null);
|
||||
}
|
||||
|
||||
ReactiveBuilder(ReactiveQueryByExampleExecutor<T> executor,
|
||||
TypeInformation<T> domainType,
|
||||
Class<R> 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 <P> ReactiveBuilder<T, P> projectAs(Class<P> 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<T, R> 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<Mono<R>> 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<Flux<R>> many() {
|
||||
return new ReactiveManyEntityFetcher<>(
|
||||
this.executor, this.domainType, this.resultType, this.sort, this.conversionService);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<R> {
|
||||
|
||||
private final QueryByExampleExecutor<T> executor;
|
||||
|
||||
private final Class<R> resultType;
|
||||
|
||||
private final Sort sort;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
SingleEntityFetcher(QueryByExampleExecutor<T> executor,
|
||||
TypeInformation<T> domainType,
|
||||
Class<R> 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<R> queryToUse = (FluentQuery.FetchableFluentQuery<R>) query;
|
||||
|
||||
if (this.sort.isSorted()) {
|
||||
queryToUse = queryToUse.sortBy(this.sort);
|
||||
}
|
||||
|
||||
Class<R> 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<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<Iterable<R>> {
|
||||
|
||||
private final QueryByExampleExecutor<T> executor;
|
||||
|
||||
private final Class<R> resultType;
|
||||
|
||||
private final Sort sort;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
ManyEntityFetcher(QueryByExampleExecutor<T> executor,
|
||||
TypeInformation<T> domainType,
|
||||
Class<R> resultType,
|
||||
Sort sort,
|
||||
ConversionService conversionService) {
|
||||
super(domainType, conversionService);
|
||||
this.executor = executor;
|
||||
this.resultType = resultType;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Iterable<R> get(DataFetchingEnvironment env) {
|
||||
return this.executor.findBy(buildExample(env), query -> {
|
||||
FluentQuery.FetchableFluentQuery<R> queryToUse = (FluentQuery.FetchableFluentQuery<R>) 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<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<Mono<R>> {
|
||||
|
||||
private final ReactiveQueryByExampleExecutor<T> executor;
|
||||
|
||||
private final Class<R> resultType;
|
||||
|
||||
private final Sort sort;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
ReactiveSingleEntityFetcher(ReactiveQueryByExampleExecutor<T> executor,
|
||||
TypeInformation<T> domainType,
|
||||
Class<R> resultType,
|
||||
Sort sort,
|
||||
ConversionService conversionService) {
|
||||
|
||||
super(domainType, conversionService);
|
||||
this.executor = executor;
|
||||
this.resultType = resultType;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<R> get(DataFetchingEnvironment env) {
|
||||
return this.executor.findBy(buildExample(env), query -> {
|
||||
FluentQuery.ReactiveFluentQuery<R> queryToUse = (FluentQuery.ReactiveFluentQuery<R>) 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<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<Flux<R>> {
|
||||
|
||||
private final ReactiveQueryByExampleExecutor<T> executor;
|
||||
|
||||
private final Class<R> resultType;
|
||||
|
||||
private final Sort sort;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
ReactiveManyEntityFetcher(ReactiveQueryByExampleExecutor<T> executor,
|
||||
TypeInformation<T> domainType,
|
||||
Class<R> resultType,
|
||||
Sort sort,
|
||||
ConversionService conversionService) {
|
||||
|
||||
super(domainType, conversionService);
|
||||
this.executor = executor;
|
||||
this.resultType = resultType;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Flux<R> get(DataFetchingEnvironment env) {
|
||||
return this.executor.findBy(buildExample(env), query -> {
|
||||
FluentQuery.ReactiveFluentQuery<R> queryToUse = (FluentQuery.ReactiveFluentQuery<R>) 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<String, Function<Boolean, DataFetcher<?>>> executorMap;
|
||||
|
||||
RegistrationTypeVisitor(
|
||||
List<QueryByExampleExecutor<?>> executors,
|
||||
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
|
||||
|
||||
this.executorMap = initExecutorMap(executors, reactiveExecutors);
|
||||
}
|
||||
|
||||
private Map<String, Function<Boolean, DataFetcher<?>>> initExecutorMap(
|
||||
List<QueryByExampleExecutor<?>> executors,
|
||||
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
|
||||
|
||||
Map<String, Function<Boolean, DataFetcher<?>>> 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<GraphQLSchemaElement> 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<Boolean, DataFetcher<?>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Book, Long> {
|
||||
}
|
||||
@@ -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<GraphQlSetup> tester = setup -> {
|
||||
Mono<WebOutput> 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<GraphQlSetup> tester = graphQlSetup -> {
|
||||
Mono<WebOutput> output = graphQlSetup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
|
||||
|
||||
List<String> 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<WebOutput> 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<WebOutput> 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<WebOutput> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Book, String> {
|
||||
}
|
||||
@@ -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<GraphQlSetup> tester = setup -> {
|
||||
Mono<WebOutput> 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<GraphQlSetup> tester = graphQlSetup -> {
|
||||
Mono<WebOutput> output = graphQlSetup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
|
||||
|
||||
List<String> 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<WebOutput> 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<WebOutput> 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<WebOutput> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GraphQlSetup> tester = setup -> {
|
||||
Mono<WebOutput> 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<WebOutput> 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<WebOutput> 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<GraphQlSetup> tester = setup -> {
|
||||
Mono<WebOutput> outputMono = setup.toWebGraphQlHandler().handleRequest(input("{ books {name}}"));
|
||||
|
||||
List<String> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Book, String> {
|
||||
}
|
||||
Reference in New Issue
Block a user