Spring Data repositories that support Querydsl
Spring Data repositories that support Querydsl are now supported as DataFetchers returning single objects and iterables including projection support. See gh-59
This commit is contained in:
committed by
Rossen Stoyanchev
parent
ab99dc2892
commit
2d53fdc73f
@@ -30,12 +30,13 @@ configure(moduleProjects) {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.3"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.7"
|
||||
mavenBom "org.springframework:spring-framework-bom:5.3.7"
|
||||
mavenBom "org.springframework.data:spring-data-bom:2021.0.1"
|
||||
mavenBom "org.junit:junit-bom:5.7.2"
|
||||
}
|
||||
dependencies {
|
||||
|
||||
@@ -18,7 +18,20 @@ dependencies {
|
||||
testImplementation project(':spring-graphql-test')
|
||||
testImplementation 'org.springframework:spring-webflux'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
implementation(
|
||||
"com.querydsl:querydsl-core:4.4.0",
|
||||
"com.querydsl:querydsl-jpa:4.4.0"
|
||||
)
|
||||
annotationProcessor "com.querydsl:querydsl-apt:4.4.0:jpa",
|
||||
"org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final",
|
||||
"javax.annotation:javax.annotation-api:1.3.2"
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.annotationProcessorPath = configurations.annotationProcessor
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package io.spring.sample.graphql.repository;
|
||||
|
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface ArtifactRepositories extends CrudRepository<ArtifactRepository, String> {
|
||||
public interface ArtifactRepositories extends CrudRepository<ArtifactRepository, String>, QuerydslPredicateExecutor<ArtifactRepository> {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package io.spring.sample.graphql.repository;
|
||||
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
|
||||
import org.springframework.graphql.boot.RuntimeWiringCustomizer;
|
||||
import org.springframework.graphql.data.QuerydslDataFetcher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -16,8 +18,10 @@ public class ArtifactRepositoryDataWiring implements RuntimeWiringCustomizer {
|
||||
@Override
|
||||
public void customize(RuntimeWiring.Builder builder) {
|
||||
builder.type("Query",
|
||||
typeWiring -> typeWiring.dataFetcher("artifactRepositories", env -> this.repositories.findAll())
|
||||
.dataFetcher("artifactRepository", env -> this.repositories.findById(env.getArgument("id"))));
|
||||
typeWiring -> typeWiring.dataFetcher("artifactRepositories", QuerydslDataFetcher
|
||||
.builder(repositories).many())
|
||||
.dataFetcher("artifactRepository", QuerydslDataFetcher
|
||||
.builder(repositories).single()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,13 +11,19 @@ dependencies {
|
||||
compileOnly 'org.springframework:spring-websocket'
|
||||
compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
|
||||
|
||||
compileOnly 'com.querydsl:querydsl-core:4.4.0'
|
||||
compileOnly 'org.springframework.data:spring-data-commons'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testImplementation 'org.assertj:assertj-core'
|
||||
testImplementation 'org.mockito:mockito-core:3.11.1'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
testImplementation 'org.springframework:spring-webflux'
|
||||
testImplementation 'org.springframework:spring-webmvc'
|
||||
testImplementation 'org.springframework:spring-websocket'
|
||||
testImplementation 'org.springframework:spring-test'
|
||||
testImplementation 'org.springframework.data:spring-data-commons'
|
||||
testImplementation 'com.querydsl:querydsl-core:4.4.0'
|
||||
testImplementation 'javax.servlet:javax.servlet-api:4.0.1'
|
||||
testImplementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PreferredConstructor;
|
||||
import org.springframework.data.mapping.PreferredConstructor.Parameter;
|
||||
import org.springframework.data.mapping.SimplePropertyHandler;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.EntityInstantiator;
|
||||
import org.springframework.data.mapping.model.EntityInstantiators;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
|
||||
/**
|
||||
* {@link Converter} to instantiate DTOs from fully equipped domain objects.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class DtoInstantiatingConverter<T> implements Converter<Object, T> {
|
||||
|
||||
private final Class<T> targetType;
|
||||
|
||||
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context;
|
||||
|
||||
private final EntityInstantiator instantiator;
|
||||
|
||||
/**
|
||||
* Create a new {@link Converter} to instantiate DTOs.
|
||||
* @param dtoType target type
|
||||
* @param context mapping context to be used
|
||||
* @param entityInstantiators the instantiators to use for object creation
|
||||
*/
|
||||
public DtoInstantiatingConverter(Class<T> dtoType,
|
||||
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
|
||||
EntityInstantiators entityInstantiators) {
|
||||
this.targetType = dtoType;
|
||||
this.context = context;
|
||||
this.instantiator = entityInstantiators
|
||||
.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T convert(Object source) {
|
||||
|
||||
if (targetType.isInterface()) {
|
||||
return (T) source;
|
||||
}
|
||||
|
||||
PersistentEntity<?, ?> sourceEntity = context
|
||||
.getRequiredPersistentEntity(source.getClass());
|
||||
|
||||
PersistentPropertyAccessor<?> sourceAccessor = sourceEntity
|
||||
.getPropertyAccessor(source);
|
||||
PersistentEntity<?, ?> targetEntity = context
|
||||
.getRequiredPersistentEntity(targetType);
|
||||
PreferredConstructor<?, ? extends PersistentProperty<?>> constructor = targetEntity
|
||||
.getPersistenceConstructor();
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
Object dto = instantiator
|
||||
.createInstance(targetEntity, new ParameterValueProvider() {
|
||||
|
||||
@Override
|
||||
public Object getParameterValue(Parameter parameter) {
|
||||
return sourceAccessor.getProperty(sourceEntity
|
||||
.getRequiredPersistentProperty(parameter.getName()));
|
||||
}
|
||||
});
|
||||
|
||||
PersistentPropertyAccessor<?> dtoAccessor = targetEntity
|
||||
.getPropertyAccessor(dto);
|
||||
|
||||
targetEntity.doWithProperties((SimplePropertyHandler) property -> {
|
||||
|
||||
if (constructor.isConstructorParameter(property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dtoAccessor.setProperty(property,
|
||||
sourceAccessor.getProperty(sourceEntity
|
||||
.getRequiredPersistentProperty(property.getName())));
|
||||
});
|
||||
|
||||
return (T) dto;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.context.AbstractMappingContext;
|
||||
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
|
||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||
import org.springframework.data.mapping.model.Property;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
/**
|
||||
* Lightweight {@link org.springframework.data.mapping.context.MappingContext}
|
||||
* to provide class metadata for entity to DTO mapping.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class DtoMappingContext extends AbstractMappingContext<DtoMappingContext.DtoPersistentEntity<?>,
|
||||
DtoMappingContext.DtoPersistentProperty> {
|
||||
|
||||
@Override
|
||||
protected boolean shouldCreatePersistentEntityFor(TypeInformation<?> type) {
|
||||
// No Java std lib type introspection to not interfere with encapsulation.
|
||||
// We do not want to get into the business of materializing Java types.
|
||||
if (type.getType().getName().startsWith("java.") || type.getType().getName()
|
||||
.startsWith("javax.")) {
|
||||
return false;
|
||||
}
|
||||
return super.shouldCreatePersistentEntityFor(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <T> DtoPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
|
||||
return new DtoPersistentEntity<>(typeInformation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DtoPersistentProperty createPersistentProperty(Property property, DtoPersistentEntity<?> owner,
|
||||
SimpleTypeHolder simpleTypeHolder) {
|
||||
return new DtoPersistentProperty(property, owner, simpleTypeHolder);
|
||||
}
|
||||
|
||||
static class DtoPersistentEntity<T> extends BasicPersistentEntity<T, DtoPersistentProperty> {
|
||||
|
||||
public DtoPersistentEntity(TypeInformation<T> information) {
|
||||
super(information);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DtoPersistentProperty extends AnnotationBasedPersistentProperty<DtoPersistentProperty> {
|
||||
|
||||
public DtoPersistentProperty(Property property, PersistentEntity<?, DtoPersistentProperty> owner,
|
||||
SimpleTypeHolder simpleTypeHolder) {
|
||||
super(property, owner, simpleTypeHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Association<DtoPersistentProperty> createAssociation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.querydsl.core.types.EntityPath;
|
||||
import com.querydsl.core.types.Predicate;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.mapping.model.EntityInstantiators;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
|
||||
import org.springframework.data.querydsl.SimpleEntityPathResolver;
|
||||
import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
|
||||
import org.springframework.data.querydsl.binding.QuerydslBindings;
|
||||
import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder;
|
||||
import org.springframework.data.repository.NoRepositoryBean;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
import org.springframework.data.util.ClassTypeInformation;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Entrypoint to create {@link DataFetcher} using repositories through Querydsl.
|
||||
* Exposes builders accepting {@link QuerydslPredicateExecutor} or
|
||||
* {@link ReactiveQuerydslPredicateExecutor} that support customization of bindings
|
||||
* and interface- and DTO projections. Instances can be created through a
|
||||
* {@link #builder(QuerydslPredicateExecutor) builder} to query for
|
||||
* {@link Builder#single()} or {@link Builder#many()} objects.
|
||||
* <p>Example:
|
||||
* <pre class="code">
|
||||
* interface BookRepository extends Repository<Book, String>,
|
||||
* QuerydslPredicateExecutor<Book>{}
|
||||
*
|
||||
* BookRepository repository = …;
|
||||
* TypeRuntimeWiring wiring = …;
|
||||
*
|
||||
* wiring.dataFetcher("books", QuerydslDataFetcher.builder(repository).many())
|
||||
* .dataFetcher("book", QuerydslDataFetcher.builder(repository).single());
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* {@link DataFetcher} returning reactive types such as {@link Mono} and {@link Flux}
|
||||
* can be constructed from a {@link ReactiveQuerydslPredicateExecutor} using
|
||||
* {@link #builder(ReactiveQuerydslPredicateExecutor) builder}.
|
||||
* <p>For example:
|
||||
* <pre class="code">
|
||||
* interface BookRepository extends Repository<Book, String>,
|
||||
* ReactiveQuerydslPredicateExecutor<Book>{}
|
||||
*
|
||||
* BookRepository repository = …;
|
||||
* TypeRuntimeWiring wiring = …;
|
||||
*
|
||||
* wiring.dataFetcher("books", QuerydslDataFetcher.builder(repository).many())
|
||||
* .dataFetcher("book", QuerydslDataFetcher.builder(repository).single());
|
||||
* </pre>
|
||||
*
|
||||
* @param <T> returned result type
|
||||
* @author Mark Paluch
|
||||
* @since 1.0.0
|
||||
* @see QuerydslPredicateExecutor
|
||||
* @see ReactiveQuerydslPredicateExecutor
|
||||
* @see Predicate
|
||||
* @see QuerydslBinderCustomizer
|
||||
*/
|
||||
public abstract class QuerydslDataFetcher<T> {
|
||||
|
||||
private static final QuerydslPredicateBuilder BUILDER = new QuerydslPredicateBuilder(
|
||||
DefaultConversionService
|
||||
.getSharedInstance(), SimpleEntityPathResolver.INSTANCE);
|
||||
|
||||
private final TypeInformation<T> domainType;
|
||||
|
||||
private final QuerydslBinderCustomizer<EntityPath<?>> customizer;
|
||||
|
||||
QuerydslDataFetcher(ClassTypeInformation<T> domainType,
|
||||
QuerydslBinderCustomizer<EntityPath<?>> customizer) {
|
||||
this.customizer = customizer;
|
||||
this.domainType = domainType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Builder} accepting {@link QuerydslPredicateExecutor}
|
||||
* to build a {@link DataFetcher}.
|
||||
* @param executor the repository object to use
|
||||
* @param <T> result type
|
||||
* @return a new builder
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> Builder<T, T> builder(QuerydslPredicateExecutor<T> executor) {
|
||||
|
||||
Class<?> repositoryInterface = getRepositoryInterface(executor);
|
||||
DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
|
||||
|
||||
return new Builder<>(executor, (ClassTypeInformation<T>) ClassTypeInformation
|
||||
.from(metadata.getDomainType()), (bindings, root) -> {
|
||||
}, Function.identity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveBuilder} accepting
|
||||
* {@link ReactiveQuerydslPredicateExecutor} to build a reactive {@link DataFetcher}.
|
||||
* @param executor the repository object to use
|
||||
* @param <T> result type
|
||||
* @return a new builder
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> ReactiveBuilder<T, T> builder(ReactiveQuerydslPredicateExecutor<T> executor) {
|
||||
|
||||
Class<?> repositoryInterface = getRepositoryInterface(executor);
|
||||
DefaultRepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
|
||||
|
||||
return new ReactiveBuilder<>(executor, (ClassTypeInformation<T>) ClassTypeInformation
|
||||
.from(metadata.getDomainType()), (bindings, root) -> {
|
||||
}, Function.identity());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
Predicate buildPredicate(DataFetchingEnvironment environment) {
|
||||
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
QuerydslBindings bindings = new QuerydslBindings();
|
||||
|
||||
EntityPath<?> path = SimpleEntityPathResolver.INSTANCE
|
||||
.createPath(domainType.getType());
|
||||
|
||||
customizer.customize(bindings, path);
|
||||
|
||||
for (Map.Entry<String, Object> entry : environment.getArguments().entrySet()) {
|
||||
parameters.put(entry.getKey(), Collections.singletonList(entry.getValue()));
|
||||
}
|
||||
|
||||
return BUILDER.getPredicate(domainType, (MultiValueMap) parameters, bindings);
|
||||
}
|
||||
|
||||
private static <S, T> Function<S, T> createProjection(Class<T> projectionType) {
|
||||
// TODO: SpelAwareProxyProjectionFactory, DtoMappingContext, and EntityInstantiators
|
||||
// should be reused to avoid duplicate class metadata.
|
||||
Assert.notNull(projectionType, "Projection type must not be null");
|
||||
|
||||
if (projectionType.isInterface()) {
|
||||
ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
|
||||
return element -> projectionFactory
|
||||
.createProjection(projectionType, element);
|
||||
}
|
||||
|
||||
DtoInstantiatingConverter<T> converter = new DtoInstantiatingConverter<>(projectionType,
|
||||
new DtoMappingContext(), new EntityInstantiators());
|
||||
return converter::convert;
|
||||
}
|
||||
|
||||
private static Class<?> getRepositoryInterface(Object executor) {
|
||||
|
||||
Assert.isInstanceOf(Repository.class, executor);
|
||||
|
||||
Type[] genericInterfaces = executor.getClass().getGenericInterfaces();
|
||||
for (Type genericInterface : genericInterfaces) {
|
||||
|
||||
ResolvableType resolvableType = ResolvableType.forType(genericInterface);
|
||||
|
||||
if (resolvableType.getRawClass() == null || MergedAnnotations
|
||||
.from(resolvableType.getRawClass())
|
||||
.isPresent(NoRepositoryBean.class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Repository.class.isAssignableFrom(resolvableType.getRawClass())) {
|
||||
return resolvableType.getRawClass();
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String
|
||||
.format("Cannot resolve repository interface from %s", executor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for a Querydsl-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 QuerydslPredicateExecutor<T> executor;
|
||||
|
||||
private final ClassTypeInformation<T> domainType;
|
||||
|
||||
private final QuerydslBinderCustomizer<? extends EntityPath<T>> customizer;
|
||||
|
||||
private final Function<T, R> resultConverter;
|
||||
|
||||
Builder(QuerydslPredicateExecutor<T> executor, ClassTypeInformation<T> domainType,
|
||||
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
|
||||
Function<T, R> resultConverter) {
|
||||
this.executor = executor;
|
||||
this.domainType = domainType;
|
||||
this.customizer = customizer;
|
||||
this.resultConverter = resultConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project results returned from the {@link QuerydslPredicateExecutor}
|
||||
* 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<>(executor, domainType, customizer, createProjection(projectionType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a {@link QuerydslBinderCustomizer}.
|
||||
* @param customizer the customizer to customize bindings for the
|
||||
* actual query
|
||||
* @return a new {@link Builder} instance with all previously configured
|
||||
* options and {@code QuerydslBinderCustomizer} applied
|
||||
*/
|
||||
public Builder<T, R> customizer(QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
|
||||
Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null");
|
||||
return new Builder<>(executor, domainType, customizer, resultConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link DataFetcher} to fetch single object instances.
|
||||
* @return a {@link DataFetcher} based on Querydsl to fetch one object
|
||||
*/
|
||||
public DataFetcher<R> single() {
|
||||
return new SingleEntityFetcher<>(executor, domainType, customizer, resultConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link DataFetcher} to fetch many object instances.
|
||||
* @return a {@link DataFetcher} based on Querydsl to fetch many objects
|
||||
*/
|
||||
public DataFetcher<Iterable<R>> many() {
|
||||
return new ManyEntityFetcher<>(executor, domainType, customizer, resultConverter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for a reactive Querydsl-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 ReactiveQuerydslPredicateExecutor<T> executor;
|
||||
|
||||
private final ClassTypeInformation<T> domainType;
|
||||
|
||||
private final QuerydslBinderCustomizer<? extends EntityPath<T>> customizer;
|
||||
|
||||
private final Function<T, R> resultConverter;
|
||||
|
||||
ReactiveBuilder(ReactiveQuerydslPredicateExecutor<T> executor,
|
||||
ClassTypeInformation<T> domainType,
|
||||
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
|
||||
Function<T, R> resultConverter) {
|
||||
this.executor = executor;
|
||||
this.domainType = domainType;
|
||||
this.customizer = customizer;
|
||||
this.resultConverter = resultConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project results returned from the {@link QuerydslPredicateExecutor}
|
||||
* 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> ReactiveBuilder<T, P> projectAs(Class<P> projectionType) {
|
||||
Assert.notNull(projectionType, "Projection type must not be null");
|
||||
return new ReactiveBuilder<>(executor, domainType, customizer, createProjection(projectionType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a {@link QuerydslBinderCustomizer}.
|
||||
* @param customizer the customizer to customize bindings for the
|
||||
* actual query
|
||||
* @return a new {@link Builder} instance with all previously configured
|
||||
* options and {@code QuerydslBinderCustomizer} applied
|
||||
*/
|
||||
public ReactiveBuilder<T, R> customizer(QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
|
||||
Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null");
|
||||
return new ReactiveBuilder<>(executor, domainType, customizer, resultConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link DataFetcher} to fetch single object instances through {@link Mono}.
|
||||
* @return a {@link DataFetcher} based on Querydsl to fetch one object
|
||||
*/
|
||||
public DataFetcher<Mono<R>> single() {
|
||||
return new ReactiveSingleEntityFetcher<>(executor, domainType, customizer, resultConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link DataFetcher} to fetch many object instances through {@link Flux}.
|
||||
* @return a {@link DataFetcher} based on Querydsl to fetch many objects
|
||||
*/
|
||||
public DataFetcher<Flux<R>> many() {
|
||||
return new ReactiveManyEntityFetcher<>(executor, domainType, customizer, resultConverter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class SingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<R> {
|
||||
|
||||
private final QuerydslPredicateExecutor<T> executor;
|
||||
|
||||
private final Function<T, R> resultConverter;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
SingleEntityFetcher(QuerydslPredicateExecutor<T> executor,
|
||||
ClassTypeInformation<T> domainType,
|
||||
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
|
||||
Function<T, R> resultConverter) {
|
||||
super(domainType, (QuerydslBinderCustomizer) customizer);
|
||||
this.executor = executor;
|
||||
this.resultConverter = resultConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R get(DataFetchingEnvironment environment) {
|
||||
return executor.findOne(buildPredicate(environment)).map(resultConverter)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ManyEntityFetcher<T, R> extends QuerydslDataFetcher<T>
|
||||
implements DataFetcher<Iterable<R>> {
|
||||
|
||||
private final QuerydslPredicateExecutor<T> executor;
|
||||
|
||||
private final Function<T, R> resultConverter;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
ManyEntityFetcher(QuerydslPredicateExecutor<T> executor,
|
||||
ClassTypeInformation<T> domainType,
|
||||
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
|
||||
Function<T, R> resultConverter) {
|
||||
super(domainType, (QuerydslBinderCustomizer) customizer);
|
||||
this.executor = executor;
|
||||
this.resultConverter = resultConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<R> get(DataFetchingEnvironment environment) {
|
||||
return Streamable.of(executor.findAll(buildPredicate(environment)))
|
||||
.map(resultConverter).toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ReactiveSingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<Mono<R>> {
|
||||
|
||||
private final ReactiveQuerydslPredicateExecutor<T> executor;
|
||||
|
||||
private final Function<T, R> resultConverter;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
ReactiveSingleEntityFetcher(ReactiveQuerydslPredicateExecutor<T> executor,
|
||||
ClassTypeInformation<T> domainType,
|
||||
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
|
||||
Function<T, R> resultConverter) {
|
||||
super(domainType, (QuerydslBinderCustomizer) customizer);
|
||||
this.executor = executor;
|
||||
this.resultConverter = resultConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<R> get(DataFetchingEnvironment environment) {
|
||||
return executor.findOne(buildPredicate(environment)).map(resultConverter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ReactiveManyEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<Flux<R>> {
|
||||
|
||||
private final ReactiveQuerydslPredicateExecutor<T> executor;
|
||||
|
||||
private final Function<T, R> resultConverter;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
ReactiveManyEntityFetcher(ReactiveQuerydslPredicateExecutor<T> executor,
|
||||
ClassTypeInformation<T> domainType,
|
||||
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer,
|
||||
Function<T, R> resultConverter) {
|
||||
super(domainType, (QuerydslBinderCustomizer) customizer);
|
||||
this.executor = executor;
|
||||
this.resultConverter = resultConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<R> get(DataFetchingEnvironment environment) {
|
||||
return executor.findAll(buildPredicate(environment)).map(resultConverter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 integrating Spring Data data fetchers.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.graphql.data;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
public class Book {
|
||||
|
||||
Long id;
|
||||
|
||||
String name;
|
||||
|
||||
String author;
|
||||
|
||||
public Book() {
|
||||
}
|
||||
|
||||
public Book(Long id, String name, String 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 String getAuthor() {
|
||||
return this.author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.querydsl.core.types.Path;
|
||||
import com.querydsl.core.types.PathMetadata;
|
||||
import com.querydsl.core.types.PathMetadataFactory;
|
||||
import com.querydsl.core.types.dsl.EntityPathBase;
|
||||
import com.querydsl.core.types.dsl.NumberPath;
|
||||
import com.querydsl.core.types.dsl.StringPath;
|
||||
|
||||
/**
|
||||
* Generated by Querydsl.
|
||||
*/
|
||||
public class QBook extends EntityPathBase<Book> {
|
||||
private static final long serialVersionUID = 1773522017L;
|
||||
public static final QBook book = new QBook("book");
|
||||
public final StringPath author = this.createString("author");
|
||||
public final NumberPath<Long> id = this.createNumber("id", Long.class);
|
||||
public final StringPath name = this.createString("name");
|
||||
|
||||
public QBook(String variable) {
|
||||
super(Book.class, PathMetadataFactory.forVariable(variable));
|
||||
}
|
||||
|
||||
public QBook(Path<? extends Book> path) {
|
||||
super(path.getType(), path.getMetadata());
|
||||
}
|
||||
|
||||
public QBook(PathMetadata metadata) {
|
||||
super(Book.class, metadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.querydsl.core.types.Predicate;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.TypeRuntimeWiring;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
|
||||
import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.graphql.execution.ExecutionGraphQlService;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.WebInput;
|
||||
import org.springframework.graphql.web.WebOutput;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link QuerydslDataFetcher}.
|
||||
*/
|
||||
class QuerydslDataFetcherTests {
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItems() {
|
||||
MockRepository mockRepository = mock(MockRepository.class);
|
||||
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams");
|
||||
when(mockRepository.findOne(any())).thenReturn(Optional.of(book));
|
||||
|
||||
WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder
|
||||
.dataFetcher("bookById", QuerydslDataFetcher
|
||||
.builder(mockRepository)
|
||||
.single()));
|
||||
|
||||
WebOutput output = handler.handle(new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections
|
||||
.singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block();
|
||||
|
||||
// TODO: getData interferes with method overrides
|
||||
assertThat((Object) output.getData())
|
||||
.isEqualTo(Collections.singletonMap("bookById", Collections
|
||||
.singletonMap("name", "Hitchhiker's Guide to the Galaxy")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchMultipleItems() {
|
||||
MockRepository mockRepository = mock(MockRepository.class);
|
||||
Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams");
|
||||
Book book2 = new Book(53L, "Breaking Bad", "Heisenberg");
|
||||
when(mockRepository.findAll((Predicate) null))
|
||||
.thenReturn(Arrays.asList(book1, book2));
|
||||
|
||||
WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder
|
||||
.dataFetcher("books", QuerydslDataFetcher
|
||||
.builder(mockRepository)
|
||||
.many()));
|
||||
|
||||
WebOutput output = handler.handle(new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections
|
||||
.singletonMap("query", "{ books {name}}"), "1")).block();
|
||||
|
||||
assertThat((Object) output.getData())
|
||||
.isEqualTo(Collections.singletonMap("books", Arrays.asList(Collections
|
||||
.singletonMap("name", "Hitchhiker's Guide to the Galaxy"), Collections
|
||||
.singletonMap("name", "Breaking Bad"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItemsWithInterfaceProjection() {
|
||||
MockRepository mockRepository = mock(MockRepository.class);
|
||||
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams");
|
||||
when(mockRepository.findOne(any())).thenReturn(Optional.of(book));
|
||||
|
||||
WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder
|
||||
.dataFetcher("bookById", QuerydslDataFetcher
|
||||
.builder(mockRepository)
|
||||
.projectAs(BookProjection.class)
|
||||
.single()));
|
||||
|
||||
WebOutput output = handler.handle(new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections
|
||||
.singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block();
|
||||
|
||||
assertThat((Object) output.getData())
|
||||
.isEqualTo(Collections.singletonMap("bookById", Collections
|
||||
.singletonMap("name", "Hitchhiker's Guide to the Galaxy by Douglas Adams")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFetchSingleItemsWithDtoProjection() {
|
||||
MockRepository mockRepository = mock(MockRepository.class);
|
||||
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams");
|
||||
when(mockRepository.findOne(any())).thenReturn(Optional.of(book));
|
||||
|
||||
WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder
|
||||
.dataFetcher("bookById", QuerydslDataFetcher
|
||||
.builder(mockRepository)
|
||||
.projectAs(BookDto.class)
|
||||
.single()));
|
||||
|
||||
WebOutput output = handler.handle(new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections
|
||||
.singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block();
|
||||
|
||||
assertThat((Object) output.getData())
|
||||
.isEqualTo(Collections.singletonMap("bookById", Collections
|
||||
.singletonMap("name", "The book is: Hitchhiker's Guide to the Galaxy")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConstructPredicateProperly() {
|
||||
MockRepository mockRepository = mock(MockRepository.class);
|
||||
|
||||
WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder
|
||||
.dataFetcher("books", QuerydslDataFetcher
|
||||
.builder(mockRepository)
|
||||
.customizer((QuerydslBinderCustomizer<QBook>) (bindings, book) -> bindings.bind(book.name)
|
||||
.firstOptional((path, value) -> value.map(path::startsWith)))
|
||||
.many()));
|
||||
|
||||
handler.handle(new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections
|
||||
.singletonMap("query", "{ books(name: \"H\", author: \"Doug\") {name}}"), "1")).block();
|
||||
|
||||
|
||||
ArgumentCaptor<Predicate> predicateCaptor = ArgumentCaptor.forClass(Predicate.class);
|
||||
verify(mockRepository).findAll(predicateCaptor.capture());
|
||||
|
||||
Predicate predicate = predicateCaptor.getValue();
|
||||
|
||||
assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H")
|
||||
.and(QBook.book.author.eq("Doug")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReactivelyFetchSingleItems() {
|
||||
ReactiveMockRepository mockRepository = mock(ReactiveMockRepository.class);
|
||||
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams");
|
||||
when(mockRepository.findOne(any())).thenReturn(Mono.just(book));
|
||||
|
||||
WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder
|
||||
.dataFetcher("bookById", QuerydslDataFetcher
|
||||
.builder(mockRepository)
|
||||
.single()));
|
||||
|
||||
WebOutput output = handler.handle(new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections
|
||||
.singletonMap("query", "{ bookById(id: 1) {name}}"), "1")).block();
|
||||
|
||||
// TODO: getData interferes with method overries
|
||||
assertThat((Object) output.getData())
|
||||
.isEqualTo(Collections.singletonMap("bookById", Collections
|
||||
.singletonMap("name", "Hitchhiker's Guide to the Galaxy")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReactivelyFetchMultipleItems() {
|
||||
ReactiveMockRepository mockRepository = mock(ReactiveMockRepository.class);
|
||||
Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", "Douglas Adams");
|
||||
Book book2 = new Book(53L, "Breaking Bad", "Heisenberg");
|
||||
when(mockRepository.findAll((Predicate) null))
|
||||
.thenReturn(Flux.just(book1, book2));
|
||||
|
||||
WebGraphQlHandler handler = initWebGraphQlHandler(builder -> builder
|
||||
.dataFetcher("books", QuerydslDataFetcher
|
||||
.builder(mockRepository)
|
||||
.many()));
|
||||
|
||||
WebOutput output = handler.handle(new WebInput(
|
||||
URI.create("http://abc.org"), new HttpHeaders(), Collections
|
||||
.singletonMap("query", "{ books {name}}"), "1")).block();
|
||||
|
||||
assertThat((Object) output.getData())
|
||||
.isEqualTo(Collections.singletonMap("books", Arrays.asList(Collections
|
||||
.singletonMap("name", "Hitchhiker's Guide to the Galaxy"), Collections
|
||||
.singletonMap("name", "Breaking Bad"))));
|
||||
}
|
||||
|
||||
interface MockRepository extends Repository<Book, Long>, QuerydslPredicateExecutor<Book> {
|
||||
|
||||
}
|
||||
|
||||
interface ReactiveMockRepository extends Repository<Book, Long>, ReactiveQuerydslPredicateExecutor<Book> {
|
||||
|
||||
}
|
||||
|
||||
static WebGraphQlHandler initWebGraphQlHandler(Consumer<TypeRuntimeWiring.Builder> configurer) {
|
||||
return WebGraphQlHandler
|
||||
.builder(new ExecutionGraphQlService(graphQlSource(configurer)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static GraphQlSource graphQlSource(Consumer<TypeRuntimeWiring.Builder> configurer) {
|
||||
RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
|
||||
TypeRuntimeWiring.Builder wiringBuilder = TypeRuntimeWiring
|
||||
.newTypeWiring("Query");
|
||||
configurer.accept(wiringBuilder);
|
||||
builder.type(wiringBuilder);
|
||||
return GraphQlSource.builder()
|
||||
.schemaResource(new ClassPathResource("books/schema.graphqls"))
|
||||
.runtimeWiring(builder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
interface BookProjection {
|
||||
|
||||
@Value("#{target.name + ' by ' + target.author}")
|
||||
String getName();
|
||||
|
||||
}
|
||||
|
||||
static class BookDto {
|
||||
|
||||
private final String name;
|
||||
|
||||
public BookDto(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "The book is: " + name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
type Query {
|
||||
bookById(id: ID): Book
|
||||
books(id: ID, name: String, author: String): [Book]
|
||||
}
|
||||
|
||||
type Book {
|
||||
|
||||
Reference in New Issue
Block a user