Pagination support for Querydsl and QBE

See gh-597
This commit is contained in:
rstoyanchev
2023-03-31 20:23:23 +01:00
parent 2d071a42ee
commit d9d67c7f90
8 changed files with 626 additions and 66 deletions

View File

@@ -12,7 +12,7 @@ dependencies {
api(platform("io.projectreactor:reactor-bom:2022.0.5"))
api(platform("io.micrometer:micrometer-bom:1.11.0-M2"))
api(platform("io.micrometer:micrometer-tracing-bom:1.1.0-M2"))
api(platform("org.springframework.data:spring-data-bom:2023.0.0-M3"))
api(platform("org.springframework.data:spring-data-bom:2023.0.0-SNAPSHOT"))
api(platform("org.springframework.security:spring-security-bom:6.1.0-M2"))
api(platform("com.querydsl:querydsl-bom:5.0.0"))
api(platform("io.rsocket:rsocket-bom:1.1.3"))

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import graphql.schema.DataFetcher;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLNamedOutputType;
import graphql.schema.GraphQLNonNull;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLType;
import graphql.schema.idl.FieldWiringEnvironment;
import graphql.schema.idl.RuntimeWiring;
@@ -76,15 +77,20 @@ class AutoRegistrationRuntimeWiringConfigurer implements RuntimeWiringConfigurer
interface DataFetcherFactory {
/**
* Create a singe item {@code DataFetcher}.
* Create {@code DataFetcher} for a singe item.
*/
DataFetcher<?> single();
/**
* Create {@code DataFetcher} for multiple items.
* Create {@code DataFetcher} for many items.
*/
DataFetcher<?> many();
/**
* Create {@code DataFetcher} for scrolling.
*/
DataFetcher<?> scrollable();
}
@@ -127,6 +133,11 @@ class AutoRegistrationRuntimeWiringConfigurer implements RuntimeWiringConfigurer
private String getOutputTypeName(FieldWiringEnvironment environment) {
GraphQLType outputType = removeNonNullWrapper(environment.getFieldType());
if (isConnectionType(outputType)) {
String name = ((GraphQLObjectType) outputType).getName();
return name.substring(0, name.length() - 10);
}
if (outputType instanceof GraphQLList) {
outputType = removeNonNullWrapper(((GraphQLList) outputType).getWrappedType());
}
@@ -142,6 +153,12 @@ class AutoRegistrationRuntimeWiringConfigurer implements RuntimeWiringConfigurer
return (outputType instanceof GraphQLNonNull wrapper ? wrapper.getWrappedType() : outputType);
}
private boolean isConnectionType(GraphQLType type) {
return (type instanceof GraphQLObjectType objectType &&
objectType.getName().endsWith("Connection") &&
objectType.getField("edges") != null && objectType.getField("pageInfo") != null);
}
private boolean hasDataFetcherFor(FieldDefinition fieldDefinition) {
if (this.existingQueryDataFetcherPredicate == null) {
Map<String, ?> map = this.builder.build().getDataFetcherForType("Query");
@@ -168,13 +185,9 @@ class AutoRegistrationRuntimeWiringConfigurer implements RuntimeWiringConfigurer
DataFetcherFactory factory = dataFetcherFactories.get(outputTypeName);
Assert.notNull(factory, "Expected DataFetcher factory for typeName '" + outputTypeName + "'");
GraphQLType outputType = removeNonNullWrapper(environment.getFieldType());
if (outputType instanceof GraphQLList) {
return factory.many();
}
else {
return factory.single();
}
GraphQLType type = removeNonNullWrapper(environment.getFieldType());
return (isConnectionType(type) ? factory.scrollable() :
(type instanceof GraphQLList ? factory.many() : factory.single()));
}
}

View File

@@ -35,13 +35,17 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
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.TypeInformation;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.pagination.CursorStrategy;
import org.springframework.graphql.data.query.AutoRegistrationRuntimeWiringConfigurer.DataFetcherFactory;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
@@ -102,9 +106,15 @@ public abstract class QueryByExampleDataFetcher<T> {
private final GraphQlArgumentBinder argumentBinder;
@Nullable
private final CursorStrategy<ScrollPosition> cursorStrategy;
QueryByExampleDataFetcher(
TypeInformation<T> domainType, @Nullable CursorStrategy<ScrollPosition> cursorStrategy) {
QueryByExampleDataFetcher(TypeInformation<T> domainType) {
this.domainType = domainType;
this.cursorStrategy = cursorStrategy;
this.argumentBinder = new GraphQlArgumentBinder();
}
@@ -154,11 +164,14 @@ public abstract class QueryByExampleDataFetcher<T> {
return Collections.emptyList();
}
protected ScrollSubrange buildScrollSubrange(DataFetchingEnvironment environment) {
return RepositoryUtils.buildScrollSubrange(environment, this.cursorStrategy);
}
/**
* Create a new {@link Builder} accepting {@link QueryByExampleExecutor}
* to build a {@link DataFetcher}.
*
* @param executor the QBE repository object to use
* @param <T> the domain type of the repository
* @return a new builder
@@ -170,7 +183,6 @@ public abstract class QueryByExampleDataFetcher<T> {
/**
* Create a new {@link ReactiveBuilder} accepting
* {@link ReactiveQueryByExampleExecutor} to build a {@link DataFetcher}.
*
* @param executor the QBE repository object to use
* @param <T> the domain type of the repository
* @return a new builder
@@ -179,6 +191,22 @@ public abstract class QueryByExampleDataFetcher<T> {
return new ReactiveBuilder<>(executor, RepositoryUtils.getDomainType(executor));
}
/**
* Variation of {@link #autoRegistrationConfigurer(List, List, CursorStrategy, ScrollSubrange)}
* that defaults to the following:
* <ul>
* <li>{@link ScrollPositionCursorStrategy} with Base64 encoding.
* <li>{@link OffsetScrollPosition Offset}-based scrolling with 20 items at a time
* </ul>
*/
public static RuntimeWiringConfigurer autoRegistrationConfigurer(
List<QueryByExampleExecutor<?>> executors,
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
return autoRegistrationConfigurer(executors, reactiveExecutors,
RepositoryUtils.defaultCursorStrategy(), RepositoryUtils.defaultScrollSubrange());
}
/**
* Return a {@link RuntimeWiringConfigurer} that installs a
* {@link graphql.schema.idl.WiringFactory} to find queries with a return
@@ -190,11 +218,16 @@ public abstract class QueryByExampleDataFetcher<T> {
*
* @param executors repositories to consider for registration
* @param reactiveExecutors reactive repositories to consider for registration
* @param cursorStrategy for decoding cursors in pagination requests
* @param defaultScrollSubrange default parameters for scrolling
* @return the created configurer
* @since 1.2
*/
public static RuntimeWiringConfigurer autoRegistrationConfigurer(
List<QueryByExampleExecutor<?>> executors,
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors) {
List<ReactiveQueryByExampleExecutor<?>> reactiveExecutors,
CursorStrategy<ScrollPosition> cursorStrategy,
ScrollSubrange defaultScrollSubrange) {
Map<String, DataFetcherFactory> factories = new HashMap<>();
@@ -212,6 +245,11 @@ public abstract class QueryByExampleDataFetcher<T> {
public DataFetcher<?> many() {
return builder.many();
}
@Override
public DataFetcher<?> scrollable() {
return builder.scrollable(cursorStrategy, defaultScrollSubrange);
}
});
}
}
@@ -230,6 +268,11 @@ public abstract class QueryByExampleDataFetcher<T> {
public DataFetcher<?> many() {
return builder.many();
}
@Override
public DataFetcher<?> scrollable() {
return builder.scrollable(cursorStrategy, defaultScrollSubrange);
}
});
}
}
@@ -363,7 +406,19 @@ public abstract class QueryByExampleDataFetcher<T> {
* Build a {@link DataFetcher} to fetch many object instances.
*/
public DataFetcher<Iterable<R>> many() {
return new ManyEntityFetcher<>(this.executor, this.domainType, this.resultType, this.sort);
return new ManyEntityFetcher<>(this.executor, this.domainType, this.resultType, null, this.sort);
}
/**
* Build a {@link DataFetcher} that scrolls and returns
* {@link org.springframework.data.domain.Window}.
* @since 1.2
*/
public DataFetcher<Iterable<R>> scrollable(
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultScrollSubrange) {
return new ScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType, cursorStrategy, defaultScrollSubrange, this.sort);
}
}
@@ -461,6 +516,18 @@ public abstract class QueryByExampleDataFetcher<T> {
return new ReactiveManyEntityFetcher<>(this.executor, this.domainType, this.resultType, this.sort);
}
/**
* Build a {@link DataFetcher} that scrolls and returns
* {@link org.springframework.data.domain.Window}.
* @since 1.2
*/
public DataFetcher<Mono<Iterable<R>>> scrollable(
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultScrollSubrange) {
return new ReactiveScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType, cursorStrategy, defaultScrollSubrange, this.sort);
}
}
/**
@@ -495,7 +562,7 @@ public abstract class QueryByExampleDataFetcher<T> {
SingleEntityFetcher(
QueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType, Sort sort) {
super(domainType);
super(domainType, null);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -539,10 +606,10 @@ public abstract class QueryByExampleDataFetcher<T> {
private final Sort sort;
ManyEntityFetcher(
QueryByExampleExecutor<T> executor, TypeInformation<T> domainType,
Class<R> resultType, Sort sort) {
QueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy, Sort sort) {
super(domainType);
super(domainType, cursorStrategy);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -565,10 +632,14 @@ public abstract class QueryByExampleDataFetcher<T> {
queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
}
return queryToUse.all();
return getResult(queryToUse, env);
});
}
protected Iterable<R> getResult(FluentQuery.FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
return queryToUse.all();
}
@Override
public ResolvableType getReturnType() {
return ResolvableType.forClassWithGenerics(Iterable.class, this.resultType);
@@ -577,6 +648,46 @@ public abstract class QueryByExampleDataFetcher<T> {
}
private static class ScrollableEntityFetcher<T, R> extends ManyEntityFetcher<T, R> {
private final ScrollSubrange defaultSubrange;
private final ResolvableType scrollableResultType;
ScrollableEntityFetcher(
QueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy,
ScrollSubrange defaultSubrange,
Sort sort) {
super(executor, domainType, resultType, cursorStrategy, sort);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
this.defaultSubrange = defaultSubrange;
this.scrollableResultType = ResolvableType.forClassWithGenerics(Window.class, resultType);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
protected Iterable<R> getResult(FluentQuery.FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
ScrollSubrange subrange = buildScrollSubrange(env);
int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt());
ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get());
return queryToUse.limit(limit).scroll(position);
}
@Override
public ResolvableType getReturnType() {
return ResolvableType.forClassWithGenerics(Iterable.class, this.scrollableResultType);
}
}
private static class ReactiveSingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements SelfDescribingDataFetcher<Mono<R>> {
private final ReactiveQueryByExampleExecutor<T> executor;
@@ -589,7 +700,7 @@ public abstract class QueryByExampleDataFetcher<T> {
ReactiveQueryByExampleExecutor<T> executor, TypeInformation<T> domainType,
Class<R> resultType, Sort sort) {
super(domainType);
super(domainType, null);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -636,7 +747,7 @@ public abstract class QueryByExampleDataFetcher<T> {
ReactiveQueryByExampleExecutor<T> executor, TypeInformation<T> domainType,
Class<R> resultType, Sort sort) {
super(domainType);
super(domainType, null);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -670,4 +781,67 @@ public abstract class QueryByExampleDataFetcher<T> {
}
private static class ReactiveScrollableEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements SelfDescribingDataFetcher<Mono<Iterable<R>>> {
private final ReactiveQueryByExampleExecutor<T> executor;
private final Class<R> resultType;
private final ResolvableType scrollableResultType;
private final ScrollSubrange defaultSubrange;
private final Sort sort;
ReactiveScrollableEntityFetcher(
ReactiveQueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultSubrange, Sort sort) {
super(domainType, cursorStrategy);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
this.executor = executor;
this.resultType = resultType;
this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType);
this.defaultSubrange = defaultSubrange;
this.sort = sort;
}
@Override
@SuppressWarnings({"unchecked", "OptionalGetWithoutIsPresent"})
public Mono<Iterable<R>> get(DataFetchingEnvironment env) throws BindException {
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));
}
ScrollSubrange subrange = buildScrollSubrange(env);
int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt());
ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get());
return queryToUse.limit(limit).scroll(position).map(Function.identity());
});
}
@Override
public ResolvableType getReturnType() {
return ResolvableType.forClassWithGenerics(Mono.class, this.scrollableResultType);
}
}
}

View File

@@ -36,6 +36,8 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor;
@@ -47,9 +49,11 @@ import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.util.TypeInformation;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.pagination.CursorStrategy;
import org.springframework.graphql.data.query.AutoRegistrationRuntimeWiringConfigurer.DataFetcherFactory;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -107,17 +111,25 @@ public abstract class QuerydslDataFetcher<T> {
DefaultConversionService.getSharedInstance(), SimpleEntityPathResolver.INSTANCE);
@SuppressWarnings("rawtypes")
private static final QuerydslBinderCustomizer NO_OP_BINDER_CUSTOMIZER = (bindings, root) -> {};
private static final QuerydslBinderCustomizer NO_OP_BINDER_CUSTOMIZER = (bindings, root) -> {
};
private final TypeInformation<T> domainType;
private final QuerydslBinderCustomizer<EntityPath<?>> customizer;
@Nullable
private final CursorStrategy<ScrollPosition> cursorStrategy;
QuerydslDataFetcher(
TypeInformation<T> domainType, QuerydslBinderCustomizer<EntityPath<?>> customizer,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy) {
QuerydslDataFetcher(TypeInformation<T> domainType, QuerydslBinderCustomizer<EntityPath<?>> customizer) {
this.domainType = domainType;
this.customizer = customizer;
this.cursorStrategy = cursorStrategy;
}
@@ -154,7 +166,7 @@ public abstract class QuerydslDataFetcher<T> {
if (environment.getFieldDefinition().getArguments().size() == 1) {
String name = environment.getFieldDefinition().getArguments().get(0).getName();
Object value = arguments.get(name);
if (value instanceof Map<?,?>) {
if (value instanceof Map<?, ?>) {
return (Map<String, Object>) value;
}
}
@@ -165,17 +177,21 @@ public abstract class QuerydslDataFetcher<T> {
return !resultType.equals(this.domainType.getType());
}
protected Collection<String> buildPropertyPaths(DataFetchingFieldSelectionSet selection, Class<?> resultType){
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)) {
this.domainType.getType().isAssignableFrom(resultType) ||
this.domainType.isSubTypeOf(resultType)) {
return PropertySelection.create(this.domainType, selection).toList();
}
return Collections.emptyList();
}
protected ScrollSubrange buildScrollSubrange(DataFetchingEnvironment environment) {
return RepositoryUtils.buildScrollSubrange(environment, this.cursorStrategy);
}
/**
* Create a new {@link Builder} accepting {@link QuerydslPredicateExecutor}
@@ -199,6 +215,22 @@ public abstract class QuerydslDataFetcher<T> {
return new ReactiveBuilder<>(executor, RepositoryUtils.getDomainType(executor));
}
/**
* Variation of {@link #autoRegistrationConfigurer(List, List, CursorStrategy, ScrollSubrange)}
* that defaults to the following:
* <ul>
* <li>{@link ScrollPositionCursorStrategy} with Base64 encoding.
* <li>{@link OffsetScrollPosition Offset}-based scrolling with 20 items at a time
* </ul>
*/
public static RuntimeWiringConfigurer autoRegistrationConfigurer(
List<QuerydslPredicateExecutor<?>> executors,
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
return autoRegistrationConfigurer(executors, reactiveExecutors,
RepositoryUtils.defaultCursorStrategy(), RepositoryUtils.defaultScrollSubrange());
}
/**
* Return a {@link RuntimeWiringConfigurer} that installs a
* {@link graphql.schema.idl.WiringFactory} to find queries with a return
@@ -214,18 +246,23 @@ public abstract class QuerydslDataFetcher<T> {
* @param executors repositories to consider for registration
* @param reactiveExecutors reactive repositories to consider for registration
* @return the created configurer
* @since 1.2
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static RuntimeWiringConfigurer autoRegistrationConfigurer(
List<QuerydslPredicateExecutor<?>> executors,
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors) {
List<ReactiveQuerydslPredicateExecutor<?>> reactiveExecutors,
CursorStrategy<ScrollPosition> cursorStrategy,
ScrollSubrange defaultScrollSubrange) {
Map<String, DataFetcherFactory> factories = new HashMap<>();
for (QuerydslPredicateExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
Builder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
Builder builder = customize(
executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, new DataFetcherFactory() {
@Override
public DataFetcher<?> single() {
@@ -236,6 +273,11 @@ public abstract class QuerydslDataFetcher<T> {
public DataFetcher<?> many() {
return builder.many();
}
@Override
public DataFetcher<?> scrollable() {
return builder.scrollable(cursorStrategy, defaultScrollSubrange);
}
});
}
}
@@ -243,7 +285,9 @@ public abstract class QuerydslDataFetcher<T> {
for (ReactiveQuerydslPredicateExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
ReactiveBuilder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
ReactiveBuilder builder = customize(
executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, new DataFetcherFactory() {
@Override
public DataFetcher<?> single() {
@@ -254,6 +298,11 @@ public abstract class QuerydslDataFetcher<T> {
public DataFetcher<?> many() {
return builder.many();
}
@Override
public DataFetcher<?> scrollable() {
return builder.scrollable(cursorStrategy, defaultScrollSubrange);
}
});
}
}
@@ -427,7 +476,20 @@ public abstract class QuerydslDataFetcher<T> {
*/
public DataFetcher<Iterable<R>> many() {
return new ManyEntityFetcher<>(
this.executor, this.domainType, this.resultType, this.sort, this.customizer);
this.executor, this.domainType, this.resultType, null, this.sort, this.customizer);
}
/**
* Build a {@link DataFetcher} that scrolls and returns
* {@link org.springframework.data.domain.Window}.
* @since 1.2
*/
public DataFetcher<Iterable<R>> scrollable(
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultScrollSubrange) {
return new ScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType, cursorStrategy, defaultScrollSubrange,
this.sort, this.customizer);
}
}
@@ -554,6 +616,19 @@ public abstract class QuerydslDataFetcher<T> {
this.executor, this.domainType, this.resultType, this.sort, this.customizer);
}
/**
* Build a {@link DataFetcher} that scrolls and returns
* {@link org.springframework.data.domain.Window}.
* @since 1.2
*/
public DataFetcher<Mono<Iterable<R>>> scrollable(
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultScrollSubrange) {
return new ReactiveScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType,
cursorStrategy, defaultScrollSubrange, this.sort, this.customizer);
}
}
@@ -593,7 +668,7 @@ public abstract class QuerydslDataFetcher<T> {
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(domainType, (QuerydslBinderCustomizer) customizer);
super(domainType, (QuerydslBinderCustomizer) customizer, null);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -640,9 +715,10 @@ public abstract class QuerydslDataFetcher<T> {
ManyEntityFetcher(QuerydslPredicateExecutor<T> executor,
TypeInformation<T> domainType,
Class<R> resultType,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy,
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(domainType, (QuerydslBinderCustomizer) customizer);
super(domainType, (QuerydslBinderCustomizer) customizer, cursorStrategy);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -665,10 +741,14 @@ public abstract class QuerydslDataFetcher<T> {
queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
}
return queryToUse.all();
return getResult(queryToUse, env);
});
}
protected Iterable<R> getResult(FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
return queryToUse.all();
}
@Override
public ResolvableType getReturnType() {
return ResolvableType.forClassWithGenerics(Iterable.class, this.resultType);
@@ -677,6 +757,40 @@ public abstract class QuerydslDataFetcher<T> {
}
private static class ScrollableEntityFetcher<T, R> extends ManyEntityFetcher<T, R> {
private final ScrollSubrange defaultSubrange;
ScrollableEntityFetcher(QuerydslPredicateExecutor<T> executor,
TypeInformation<T> domainType,
Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy,
ScrollSubrange defaultSubrange,
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(executor, domainType, resultType, cursorStrategy, sort, customizer);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
this.defaultSubrange = defaultSubrange;
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
protected Iterable<R> getResult(FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
ScrollSubrange subrange = buildScrollSubrange(env);
int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt());
ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get());
return queryToUse.limit(limit).scroll(position);
}
}
private static class ReactiveSingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements SelfDescribingDataFetcher<Mono<R>> {
private final ReactiveQuerydslPredicateExecutor<T> executor;
@@ -692,7 +806,7 @@ public abstract class QuerydslDataFetcher<T> {
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(domainType, (QuerydslBinderCustomizer) customizer);
super(domainType, (QuerydslBinderCustomizer) customizer, null);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -742,7 +856,7 @@ public abstract class QuerydslDataFetcher<T> {
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(domainType, (QuerydslBinderCustomizer) customizer);
super(domainType, (QuerydslBinderCustomizer) customizer, null);
this.executor = executor;
this.resultType = resultType;
this.sort = sort;
@@ -776,4 +890,71 @@ public abstract class QuerydslDataFetcher<T> {
}
private static class ReactiveScrollableEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements SelfDescribingDataFetcher<Mono<Iterable<R>>> {
private final ReactiveQuerydslPredicateExecutor<T> executor;
private final Class<R> resultType;
private final ResolvableType scrollableResultType;
private final ScrollSubrange defaultSubrange;
private final Sort sort;
@SuppressWarnings({"unchecked", "rawtypes"})
ReactiveScrollableEntityFetcher(ReactiveQuerydslPredicateExecutor<T> executor,
TypeInformation<T> domainType,
Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultSubrange,
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(domainType, (QuerydslBinderCustomizer) customizer, cursorStrategy);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
this.executor = executor;
this.resultType = resultType;
this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType);
this.defaultSubrange = defaultSubrange;
this.sort = sort;
}
@Override
@SuppressWarnings({"unchecked", "OptionalGetWithoutIsPresent"})
public Mono<Iterable<R>> get(DataFetchingEnvironment env) {
return this.executor.findBy(buildPredicate(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));
}
ScrollSubrange subrange = buildScrollSubrange(env);
int limit = subrange.count().orElse(this.defaultSubrange.count().getAsInt());
ScrollPosition position = subrange.position().orElse(this.defaultSubrange.position().get());
return queryToUse.limit(limit).scroll(position).map(Function.identity());
});
}
@Override
public ResolvableType getReturnType() {
return ResolvableType.forClassWithGenerics(Mono.class, this.scrollableResultType);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 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.
@@ -17,14 +17,20 @@ package org.springframework.graphql.data.query;
import java.lang.reflect.Type;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
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.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.pagination.CursorEncoder;
import org.springframework.graphql.data.pagination.CursorStrategy;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -73,4 +79,24 @@ class RepositoryUtils {
annotation.typeName() : RepositoryUtils.getDomainType(repository).getSimpleName());
}
public static CursorStrategy<ScrollPosition> defaultCursorStrategy() {
return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
}
public static ScrollSubrange defaultScrollSubrange() {
return new ScrollSubrange(OffsetScrollPosition.initial(), 20, true);
}
public static ScrollSubrange buildScrollSubrange(
DataFetchingEnvironment environment, CursorStrategy<ScrollPosition> cursorStrategy) {
Assert.notNull(cursorStrategy, "CursorStrategy is required to build a ScrollSubrange");
boolean forward = !environment.getArguments().containsKey("last");
Integer count = environment.getArgument(forward ? "first" : "last");
String cursor = environment.getArgument(forward ? "after" : "before");
ScrollPosition position = (cursor != null ? cursorStrategy.fromCursor(cursor) : null);
return new ScrollSubrange(position, count, forward);
}
}

View File

@@ -33,6 +33,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
import org.springframework.data.map.MapKeyValueAdapter;
@@ -48,8 +49,10 @@ import org.springframework.graphql.ExecutionGraphQlResponse;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
import org.springframework.graphql.data.query.QuerydslDataFetcher.Builder;
import org.springframework.graphql.data.query.QuerydslDataFetcher.QuerydslBuilderCustomizer;
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlRequest;
@@ -117,6 +120,48 @@ class QuerydslDataFetcherTests {
tester.accept(graphQlSetup(mockRepository));
}
@Test
void shouldFetchWindow() {
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"));
mockRepository.saveAll(Arrays.asList(book1, book2));
Consumer<GraphQlSetup> tester = graphQlSetup -> {
Mono<WebGraphQlResponse> response = graphQlSetup
.toWebGraphQlHandler()
.handleRequest(request(BookSource.booksConnectionQuery("")));
ResponseHelper.forResponse(response).assertData(
"{\"books\":{" +
"\"edges\":[" +
"{\"cursor\":\"O_4\",\"node\":{\"id\":\"42\",\"name\":\"Hitchhiker's Guide to the Galaxy\"}}," +
"{\"cursor\":\"O_5\",\"node\":{\"id\":\"53\",\"name\":\"Breaking Bad\"}}" +
"]," +
"\"pageInfo\":{" +
"\"startCursor\":\"O_4\"," +
"\"endCursor\":\"O_5\"," +
"\"hasPreviousPage\":true," +
"\"hasNextPage\":false" +
"}}}"
);
};
// explicit wiring
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy();
DataFetcher<Iterable<Book>> dataFetcher = QuerydslDataFetcher.builder(mockRepository)
.scrollable(cursorStrategy, new ScrollSubrange(OffsetScrollPosition.initial(), 10, true));
GraphQlSetup graphQlSetup = paginationSetup(cursorStrategy).queryFetcher("books", dataFetcher);
tester.accept(graphQlSetup);
// auto registration
graphQlSetup = paginationSetup(cursorStrategy).runtimeWiring(createRuntimeWiringConfigurer(mockRepository, null));
tester.accept(graphQlSetup);
}
@Test
void shouldFetchMultipleItemsWithListInput() {
Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
@@ -230,7 +275,7 @@ class QuerydslDataFetcherTests {
};
// explicit wiring
tester.accept(initGraphQlSetup(mockWithCustomizerRepository, null));
tester.accept(graphQlSetup(mockWithCustomizerRepository));
}
@Test
@@ -297,27 +342,33 @@ class QuerydslDataFetcherTests {
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
}
static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
return initGraphQlSetup(null, null).queryFetcher(fieldName, fetcher);
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
return GraphQlSetup.schemaResource(BookSource.schema).queryFetcher(fieldName, fetcher);
}
static GraphQlSetup graphQlSetup(@Nullable QuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(executor, null);
private static GraphQlSetup graphQlSetup(@Nullable QuerydslPredicateExecutor<?> executor) {
return GraphQlSetup.schemaResource(BookSource.schema)
.runtimeWiring(createRuntimeWiringConfigurer(executor, null));
}
static GraphQlSetup graphQlSetup(@Nullable ReactiveQuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(null, executor);
private static GraphQlSetup graphQlSetup(@Nullable ReactiveQuerydslPredicateExecutor<?> executor) {
return GraphQlSetup.schemaResource(BookSource.schema)
.runtimeWiring(createRuntimeWiringConfigurer(null, executor));
}
private static GraphQlSetup initGraphQlSetup(
private static GraphQlSetup paginationSetup(ScrollPositionCursorStrategy cursorStrategy) {
return GraphQlSetup.schemaResource(BookSource.paginationSchema)
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer())
.typeVisitor(ConnectionFieldTypeVisitor.create(List.of(new WindowConnectionAdapter(cursorStrategy))));
}
private static RuntimeWiringConfigurer createRuntimeWiringConfigurer(
@Nullable QuerydslPredicateExecutor<?> executor,
@Nullable ReactiveQuerydslPredicateExecutor<?> reactiveExecutor) {
RuntimeWiringConfigurer configurer = QuerydslDataFetcher.autoRegistrationConfigurer(
return QuerydslDataFetcher.autoRegistrationConfigurer(
(executor != null ? Collections.singletonList(executor) : Collections.emptyList()),
(reactiveExecutor != null ? Collections.singletonList(reactiveExecutor) : Collections.emptyList()));
return GraphQlSetup.schemaResource(BookSource.schema).runtimeWiring(configurer);
}
private WebGraphQlRequest request(String query) {

View File

@@ -35,20 +35,25 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.graphql.BookSource;
import org.springframework.graphql.ExecutionGraphQlResponse;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
import org.springframework.graphql.data.query.ScrollSubrange;
import org.springframework.graphql.data.query.WindowConnectionAdapter;
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
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;
@@ -118,6 +123,52 @@ class QueryByExampleDataFetcherJpaTests {
tester.accept(graphQlSetup(repository));
}
@Test
void shouldFetchWindow() {
repository.saveAll(List.of(
new Book(1L, "Nineteen Eighty-Four", new Author(0L, "George", "Orwell")),
new Book(2L, "The Great Gatsby", new Author(0L, "F. Scott", "Fitzgerald")),
new Book(3L, "Catch-22", new Author(0L, "Joseph", "Heller")),
new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams")),
new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"))));
Consumer<GraphQlSetup> tester = graphQlSetup -> {
Mono<WebGraphQlResponse> response = graphQlSetup
.toWebGraphQlHandler()
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\"")));
ResponseHelper.forResponse(response).assertData(
"{\"books\":{" +
"\"edges\":[" +
"{\"cursor\":\"O_4\",\"node\":{\"id\":\"42\",\"name\":\"Hitchhiker's Guide to the Galaxy\"}}," +
"{\"cursor\":\"O_5\",\"node\":{\"id\":\"53\",\"name\":\"Breaking Bad\"}}" +
"]," +
"\"pageInfo\":{" +
"\"startCursor\":\"O_4\"," +
"\"endCursor\":\"O_5\"," +
"\"hasPreviousPage\":true," +
"\"hasNextPage\":false" +
"}}}"
);
};
// explicit wiring
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy();
DataFetcher<Iterable<Book>> dataFetcher = QueryByExampleDataFetcher.builder(repository)
.scrollable(cursorStrategy, new ScrollSubrange(OffsetScrollPosition.initial(), 10, true));
GraphQlSetup graphQlSetup = paginationSetup(cursorStrategy).queryFetcher("books", dataFetcher);
tester.accept(graphQlSetup);
// auto registration
graphQlSetup = paginationSetup(cursorStrategy).runtimeWiring(createRuntimeWiringConfigurer(repository));
tester.accept(graphQlSetup);
}
@Test
void shouldFavorExplicitWiring() {
BookJpaRepository mockRepository = mock(BookJpaRepository.class);
@@ -190,20 +241,26 @@ class QueryByExampleDataFetcherJpaTests {
}
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
return initGraphQlSetup(null).queryFetcher(fieldName, fetcher);
return GraphQlSetup.schemaResource(BookSource.schema).queryFetcher(fieldName, fetcher);
}
private static GraphQlSetup graphQlSetup(@Nullable QueryByExampleExecutor<?> executor) {
return initGraphQlSetup(executor);
private static GraphQlSetup graphQlSetup(QueryByExampleExecutor<?> executor) {
return GraphQlSetup.schemaResource(BookSource.schema)
.runtimeWiring(createRuntimeWiringConfigurer(executor));
}
private static GraphQlSetup initGraphQlSetup(@Nullable QueryByExampleExecutor<?> executor) {
private static GraphQlSetup paginationSetup(ScrollPositionCursorStrategy cursorStrategy) {
return GraphQlSetup.schemaResource(BookSource.paginationSchema)
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer())
.typeVisitor(ConnectionFieldTypeVisitor.create(List.of(new WindowConnectionAdapter(cursorStrategy))));
}
RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher.autoRegistrationConfigurer(
private static RuntimeWiringConfigurer createRuntimeWiringConfigurer(QueryByExampleExecutor<?> executor) {
return QueryByExampleDataFetcher.autoRegistrationConfigurer(
executor != null ? Collections.singletonList(executor) : Collections.emptyList(),
Collections.emptyList());
return GraphQlSetup.schemaResource(BookSource.schema).runtimeWiring(configurer);
Collections.emptyList(),
new ScrollPositionCursorStrategy(),
new ScrollSubrange(OffsetScrollPosition.initial(), 10, true));
}
private WebGraphQlRequest request(String query) {

View File

@@ -37,13 +37,19 @@ 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.domain.OffsetScrollPosition;
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.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
import org.springframework.graphql.data.pagination.ConnectionFieldTypeVisitor;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
import org.springframework.graphql.data.query.ScrollPositionCursorStrategy;
import org.springframework.graphql.data.query.ScrollSubrange;
import org.springframework.graphql.data.query.WindowConnectionAdapter;
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlRequest;
@@ -114,6 +120,52 @@ class QueryByExampleDataFetcherMongoDbTests {
tester.accept(graphQlSetup(repository));
}
@Test
void shouldFetchWindow() {
repository.saveAll(List.of(
new Book("1", "Nineteen Eighty-Four", new Author("0", "George", "Orwell")),
new Book("2", "The Great Gatsby", new Author("0", "F. Scott", "Fitzgerald")),
new Book("3", "Catch-22", new Author("0", "Joseph", "Heller")),
new Book("42", "Hitchhiker's Guide to the Galaxy", new Author("0", "Douglas", "Adams")),
new Book("53", "Breaking Bad", new Author("0", "", "Heisenberg"))));
Consumer<GraphQlSetup> tester = graphQlSetup -> {
Mono<WebGraphQlResponse> response = graphQlSetup
.toWebGraphQlHandler()
.handleRequest(request(BookSource.booksConnectionQuery("first:2, after:\"O_3\"")));
ResponseHelper.forResponse(response).assertData(
"{\"books\":{" +
"\"edges\":[" +
"{\"cursor\":\"O_4\",\"node\":{\"id\":\"42\",\"name\":\"Hitchhiker's Guide to the Galaxy\"}}," +
"{\"cursor\":\"O_5\",\"node\":{\"id\":\"53\",\"name\":\"Breaking Bad\"}}" +
"]," +
"\"pageInfo\":{" +
"\"startCursor\":\"O_4\"," +
"\"endCursor\":\"O_5\"," +
"\"hasPreviousPage\":true," +
"\"hasNextPage\":false" +
"}}}"
);
};
// explicit wiring
ScrollPositionCursorStrategy cursorStrategy = new ScrollPositionCursorStrategy();
DataFetcher<Iterable<Book>> dataFetcher = QueryByExampleDataFetcher.builder(repository)
.scrollable(cursorStrategy, new ScrollSubrange(OffsetScrollPosition.initial(), 10, true));
GraphQlSetup graphQlSetup = paginationSetup(cursorStrategy).queryFetcher("books", dataFetcher);
tester.accept(graphQlSetup);
// auto registration
graphQlSetup = paginationSetup(cursorStrategy).runtimeWiring(createRuntimeWiringConfigurer(repository));
tester.accept(graphQlSetup);
}
@Test
void shouldFavorExplicitWiring() {
BookMongoRepository mockRepository = mock(BookMongoRepository.class);
@@ -167,20 +219,26 @@ class QueryByExampleDataFetcherMongoDbTests {
}
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
return initGraphQlSetup(null).queryFetcher(fieldName, fetcher);
return GraphQlSetup.schemaResource(BookSource.schema).queryFetcher(fieldName, fetcher);
}
private static GraphQlSetup graphQlSetup(@Nullable QueryByExampleExecutor<?> executor) {
return initGraphQlSetup(executor);
return GraphQlSetup.schemaResource(BookSource.schema)
.runtimeWiring(createRuntimeWiringConfigurer(executor));
}
private static GraphQlSetup initGraphQlSetup(@Nullable QueryByExampleExecutor<?> executor) {
private static GraphQlSetup paginationSetup(ScrollPositionCursorStrategy cursorStrategy) {
return GraphQlSetup.schemaResource(BookSource.paginationSchema)
.typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer())
.typeVisitor(ConnectionFieldTypeVisitor.create(List.of(new WindowConnectionAdapter(cursorStrategy))));
}
RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher.autoRegistrationConfigurer(
private static RuntimeWiringConfigurer createRuntimeWiringConfigurer(QueryByExampleExecutor<?> executor) {
return QueryByExampleDataFetcher.autoRegistrationConfigurer(
(executor != null ? Collections.singletonList(executor) : Collections.emptyList()),
Collections.emptyList());
return GraphQlSetup.schemaResource(BookSource.schema).runtimeWiring(configurer);
Collections.emptyList(),
new ScrollPositionCursorStrategy(),
new ScrollSubrange(OffsetScrollPosition.initial(), 10, true));
}
private WebGraphQlRequest request(String query) {