Skip to content

Introduce DataFetcher builder customizers for Querydsl and Query by Example repositories #559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions spring-graphql-docs/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -967,11 +967,10 @@ obtained from `QuerydslDataFetcher`. The
detects `@GraphQlRepository` beans and uses them to initialize the
`RuntimeWiringConfigurer` with.

Auto-registration does not support <<data-querybyexample-customizations, customizations>>.
If you need that, you'll need to use `QueryByExampleDataFetcher` to build and
register the `DataFetcher` manually through a
<<execution-graphqlsource-runtimewiring-configurer>>.

Auto-registration applies <<data-querybyexample-customizations, customizations>>
by calling `customize(Builder)` on the repository instance if your repository
implements `QuerydslBuilderCustomizer` or `ReactiveQuerydslBuilderCustomizer`
respectively.


[[data-querybyexample]]
Expand Down Expand Up @@ -1081,10 +1080,10 @@ obtained from `QueryByExampleDataFetcher`. The
detects `@GraphQlRepository` beans and uses them to initialize the
`RuntimeWiringConfigurer` with.

Auto-registration does not support <<data-querybyexample-customizations, customizations>>.
If you need that, you'll need to use `QueryByExampleDataFetcher` to build and
register the `DataFetcher` manually through a
<<execution-graphqlsource-runtimewiring-configurer>>.
Auto-registration applies <<data-querybyexample-customizations, customizations>>
by calling `customize(Builder)` on the repository instance if your repository
implements `QueryByExampleBuilderCustomizer` or
`ReactiveQueryByExampleBuilderCustomizer` respectively.



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,16 @@ public static RuntimeWiringConfigurer autoRegistrationConfigurer(
for (QueryByExampleExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
Builder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}

for (ReactiveQueryByExampleExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
ReactiveBuilder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}

Expand Down Expand Up @@ -218,21 +220,40 @@ public static GraphQLTypeVisitor autoRegistrationTypeVisitor(
for (QueryByExampleExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
Builder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}

for (ReactiveQueryByExampleExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
factories.put(typeName, single -> single ? builder(executor).single() : builder(executor).many());
ReactiveBuilder<?, ?> builder = customize(executor, builder(executor));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}

return new AutoRegistrationTypeVisitor(factories);
}


@SuppressWarnings({"unchecked", "rawtypes"})
private static Builder customize(QueryByExampleExecutor<?> executor, Builder builder) {
if(executor instanceof QueryByExampleBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}

@SuppressWarnings({"unchecked", "rawtypes"})
private static ReactiveBuilder customize(ReactiveQueryByExampleExecutor<?> executor, ReactiveBuilder builder) {
if(executor instanceof ReactiveQueryByExampleBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}


/**
* Builder for a Query by Example-based {@link DataFetcher}. Note that builder
* instances are immutable and return a new instance of the builder
Expand Down Expand Up @@ -304,6 +325,24 @@ public DataFetcher<Iterable<R>> many() {

}

/**
* Callback interface that can be used to customize QueryByExampleDataFetcher {@link Builder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface QueryByExampleBuilderCustomizer<T> {

/**
* Callback to customize a {@link Builder} instance.
* @param builder builder to customize
*/
Builder<T, ?> customize(Builder<T, ?> builder);

}


/**
* Builder for a reactive Query by Example-based {@link DataFetcher}.
Expand Down Expand Up @@ -379,6 +418,24 @@ public DataFetcher<Flux<R>> many() {

}

/**
* Callback interface that can be used to customize QueryByExampleDataFetcher {@link ReactiveBuilder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface ReactiveQueryByExampleBuilderCustomizer<T> {

/**
* Callback to customize a {@link ReactiveBuilder} instance.
* @param builder builder to customize
*/
ReactiveBuilder<T, ?> customize(ReactiveBuilder<T, ?> builder);

}


private static class SingleEntityFetcher<T, R> extends QueryByExampleDataFetcher<T> implements DataFetcher<R> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,15 @@ public static RuntimeWiringConfigurer autoRegistrationConfigurer(
for (QuerydslPredicateExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
Builder<?, ?> builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
Builder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}

for (ReactiveQuerydslPredicateExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
ReactiveBuilder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
ReactiveBuilder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}
Expand Down Expand Up @@ -253,22 +253,38 @@ public static GraphQLTypeVisitor autoRegistrationTypeVisitor(
for (QuerydslPredicateExecutor<?> executor : executors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
Builder<?, ?> builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
Builder<?, ?> builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}

for (ReactiveQuerydslPredicateExecutor<?> executor : reactiveExecutors) {
String typeName = RepositoryUtils.getGraphQlTypeName(executor);
if (typeName != null) {
ReactiveBuilder builder = QuerydslDataFetcher.builder(executor).customizer(customizer(executor));
ReactiveBuilder builder = customize(executor, QuerydslDataFetcher.builder(executor).customizer(customizer(executor)));
factories.put(typeName, single -> single ? builder.single() : builder.many());
}
}

return new AutoRegistrationTypeVisitor(factories);
}

@SuppressWarnings({"unchecked", "rawtypes"})
private static Builder customize(QuerydslPredicateExecutor<?> executor, Builder builder) {
if(executor instanceof QuerydslBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}

@SuppressWarnings({"unchecked", "rawtypes"})
private static ReactiveBuilder customize(ReactiveQuerydslPredicateExecutor<?> executor, ReactiveBuilder builder) {
if(executor instanceof ReactiveQuerydslBuilderCustomizer<?> customizer){
return customizer.customize(builder);
}
return builder;
}

@SuppressWarnings("rawtypes")
private static QuerydslBinderCustomizer customizer(Object executor) {
return (executor instanceof QuerydslBinderCustomizer<?> ?
Expand Down Expand Up @@ -377,6 +393,25 @@ public DataFetcher<Iterable<R>> many() {
}


/**
* Callback interface that can be used to customize QuerydslDataFetcher {@link Builder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface QuerydslBuilderCustomizer<T> {

/**
* Callback to customize a {@link Builder} instance.
* @param builder builder to customize
*/
Builder<T, ?> customize(Builder<T, ?> builder);

}


/**
* Builder for a reactive Querydsl-based {@link DataFetcher}. Note that builder
* instances are immutable and return a new instance of the builder when
Expand Down Expand Up @@ -480,6 +515,25 @@ public DataFetcher<Flux<R>> many() {
}


/**
* Callback interface that can be used to customize QuerydslDataFetcher {@link ReactiveBuilder}
* to change its configuration. {@link #autoRegistrationConfigurer(List, List) Auto-registration}
* applies the customizer for DataFetchers based on repositories implementing this interface.
*
* @param <T>
* @since 1.1.1
*/
public interface ReactiveQuerydslBuilderCustomizer<T> {

/**
* Callback to customize a {@link ReactiveBuilder} instance.
* @param builder builder to customize
*/
ReactiveBuilder<T, ?> customize(ReactiveBuilder<T, ?> builder);

}


private static class SingleEntityFetcher<T, R> extends QuerydslDataFetcher<T> implements DataFetcher<R> {

private final QuerydslPredicateExecutor<T> executor;
Expand Down Expand Up @@ -659,5 +713,4 @@ public Flux<R> get(DataFetchingEnvironment env) {

}


}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.query.QuerydslDataFetcher.Builder;
import org.springframework.graphql.data.query.QuerydslDataFetcher.QuerydslBuilderCustomizer;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlHandler;
Expand Down Expand Up @@ -213,16 +215,21 @@ void shouldFetchSingleItemsWithInterfaceProjection() {

@Test
void shouldFetchSingleItemsWithDtoProjection() {
MockWithBuilderCustomizerRepository mockWithCustomizerRepository = repositoryFactory.getRepository(MockWithBuilderCustomizerRepository.class);
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
mockRepository.save(book);
mockWithCustomizerRepository.save(book);

DataFetcher<?> fetcher = QuerydslDataFetcher.builder(mockRepository).projectAs(BookDto.class).single();
WebGraphQlHandler handler = graphQlSetup("bookById", fetcher).toWebGraphQlHandler();
Consumer<GraphQlSetup> tester = graphQlSetup -> {
WebGraphQlRequest request = request("{ bookById(id: 42) {name}}");
Mono<WebGraphQlResponse> responseMono = graphQlSetup.toWebGraphQlHandler().handleRequest(request);

Mono<WebGraphQlResponse> responseMono = handler.handleRequest(request("{ bookById(id: 42) {name}}"));
Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);

Book actualBook = ResponseHelper.forResponse(responseMono).toEntity("bookById", Book.class);
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
assertThat(actualBook.getName()).isEqualTo("The book is: " + book.getName());
};

// explicit wiring
tester.accept(initGraphQlSetup(mockWithCustomizerRepository, null));
}

@Test
Expand Down Expand Up @@ -275,11 +282,11 @@ static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
}

static GraphQlSetup graphQlSetup(@Nullable QuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(executor, null);
return initGraphQlSetup(executor, null);
}

static GraphQlSetup graphQlSetup(@Nullable ReactiveQuerydslPredicateExecutor<?> executor) {
return initGraphQlSetup(null, executor);
return initGraphQlSetup(null, executor);
}

private static GraphQlSetup initGraphQlSetup(
Expand All @@ -303,6 +310,15 @@ private WebGraphQlRequest request(String query) {
interface MockRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book> {
}

@GraphQlRepository
interface MockWithBuilderCustomizerRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book>, QuerydslBuilderCustomizer<Book> {

@Override
default Builder<Book, ?> customize(Builder<Book, ?> builder) {
return builder.projectAs(BookDto.class);
}
}


@GraphQlRepository
interface MockWithCustomizerRepository extends CrudRepository<Book, Long>, QuerydslPredicateExecutor<Book>,
Expand All @@ -328,6 +344,7 @@ interface BookProjection {

}


static class BookDto {

private final String name;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.graphql.data.query.jpa;

import org.springframework.beans.factory.annotation.Value;

interface BookProjection {

@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")
String getName();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.graphql.data.query.jpa;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.graphql.data.GraphQlRepository;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher.Builder;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher.QueryByExampleBuilderCustomizer;
import org.springframework.graphql.data.query.jpa.QueryByExampleDataFetcherJpaTests.BookDto;

@GraphQlRepository
public interface ProjectingBookJpaRepository extends JpaRepository<Book, Long>, QueryByExampleBuilderCustomizer<Book> {

@Override
default Builder<Book, ?> customize(Builder<Book, ?> builder){
return builder.projectAs(BookProjection.class);
}
}
Loading