Querydsl and QBE nest into single argument input type

Closes gh-216
This commit is contained in:
rstoyanchev
2023-03-22 16:29:31 +00:00
parent 4ae21a8b5a
commit 911b78237c
6 changed files with 96 additions and 13 deletions

View File

@@ -865,9 +865,11 @@ Then use it to create a `DataFetcher`:
You can now register the above `DataFetcher` through a
<<execution.graphqlsource.runtimewiring-configurer>>.
The `DataFetcher` builds a Querydsl `Predicate` from GraphQL request parameters, and
uses it to fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA,
MongoDB, and LDAP.
The `DataFetcher` builds a Querydsl `Predicate` from GraphQL arguments, and uses it to
fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA, MongoDB, and LDAP.
NOTE: For a single argument that is a GraphQL input type, `QuerydslDataFetcher` nests one
level down, and uses the values from the argument sub-map.
If the repository is `ReactiveQuerydslPredicateExecutor`, the builder returns
`DataFetcher<Mono<Account>>` or `DataFetcher<Flux<Account>>`. Spring Data supports this
@@ -1058,6 +1060,9 @@ The `DataFetcher` uses the GraphQL arguments map to create the domain type of th
repository and use that as the example object to fetch data with. Spring Data supports
`QueryByExampleDataFetcher` for JPA, MongoDB, Neo4j, and Redis.
NOTE: For a single argument that is a GraphQL input type, `QueryByExampleDataFetcher`
nests one level down, and binds with the values from the argument sub-map.
If the repository is `ReactiveQueryByExampleExecutor`, the builder returns
`DataFetcher<Mono<Account>>` or `DataFetcher<Flux<Account>>`. Spring Data supports this
variant for MongoDB, Neo4j, Redis, and R2dbc.

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.
@@ -26,6 +26,7 @@ import java.util.function.Function;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingFieldSelectionSet;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLTypeVisitor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -41,8 +42,9 @@ 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.execution.SelfDescribingDataFetcher;
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.validation.BindException;
@@ -108,13 +110,32 @@ public abstract class QueryByExampleDataFetcher<T> {
/**
* Prepare an {@link Example} from GraphQL request arguments.
* @param env contextual info for the GraphQL request
* @param environment contextual info for the GraphQL request
* @return the resulting example
*/
@SuppressWarnings({"ConstantConditions", "unchecked"})
protected Example<T> buildExample(DataFetchingEnvironment env) throws BindException {
protected Example<T> buildExample(DataFetchingEnvironment environment) throws BindException {
String name = getArgumentName(environment);
ResolvableType targetType = ResolvableType.forClass(this.domainType.getType());
return (Example<T>) Example.of(this.argumentBinder.bind(env, null, targetType));
return (Example<T>) Example.of(this.argumentBinder.bind(environment, name, targetType));
}
/**
* For a single argument that is a GraphQL input type, return the argument
* name, thereby nesting and having the example Object populated from the
* sub-map. Otherwise, {@code null} to bind using the top-level map.
*/
@Nullable
private static String getArgumentName(DataFetchingEnvironment environment) {
Map<String, Object> arguments = environment.getArguments();
List<GraphQLArgument> definedArguments = environment.getFieldDefinition().getArguments();
if (definedArguments.size() == 1) {
String name = definedArguments.get(0).getName();
if (arguments.get(name) instanceof Map<?,?>) {
return name;
}
}
return null;
}
protected boolean requiresProjection(Class<?> resultType) {

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.
@@ -47,8 +47,8 @@ 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.execution.SelfDescribingDataFetcher;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -134,7 +134,7 @@ public abstract class QuerydslDataFetcher<T> {
EntityPath<?> path = SimpleEntityPathResolver.INSTANCE.createPath(this.domainType.getType());
this.customizer.customize(bindings, path);
for (Map.Entry<String, Object> entry : environment.getArguments().entrySet()) {
for (Map.Entry<String, Object> entry : getArgumentValues(environment).entrySet()) {
Object value = entry.getValue();
List<Object> values = (value instanceof List ? (List<Object>) value : Collections.singletonList(value));
parameters.put(entry.getKey(), values);
@@ -143,6 +143,23 @@ public abstract class QuerydslDataFetcher<T> {
return BUILDER.getPredicate(this.domainType, parameters, bindings);
}
/**
* For a single argument that is a GraphQL input type, return the sub-map
* under the argument name, or otherwise the top-level argument map.
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> getArgumentValues(DataFetchingEnvironment environment) {
Map<String, Object> arguments = environment.getArguments();
if (environment.getFieldDefinition().getArguments().size() == 1) {
String name = environment.getFieldDefinition().getArguments().get(0).getName();
Object value = arguments.get(name);
if (value instanceof Map<?,?>) {
return (Map<String, Object>) value;
}
}
return arguments;
}
protected boolean requiresProjection(Class<?> resultType) {
return !resultType.equals(this.domainType.getType());
}

View File

@@ -106,7 +106,7 @@ public class ResponseHelper {
private void assertNoErrors() {
if (!this.errorsChecked) {
assertThat(this.errors).as("Errors present in GraphQL response").isEmpty();
assertThat(this.errors).as("GraphQL response errors: " + this.errors).isEmpty();
this.errorsChecked = true;
}
}

View File

@@ -44,6 +44,7 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
import org.springframework.graphql.Author;
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.GraphQlRepository;
@@ -74,7 +75,7 @@ class QuerydslDataFetcherTests {
@Test
void shouldFetchSingleItems() {
void shouldFetchSingleItem() {
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
mockRepository.save(book);
@@ -277,6 +278,25 @@ class QuerydslDataFetcherTests {
tester.accept(graphQlSetup(mockRepository));
}
@Test
void shouldNestForSingleArgumentInputType() {
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));
String queryName = "booksByCriteria";
Mono<ExecutionGraphQlResponse> responseMono =
graphQlSetup(queryName, QuerydslDataFetcher.builder(mockRepository).many())
.toGraphQlService()
.execute(request("{" + queryName + "(criteria: {id: 42}) {name}}"));
List<Book> books = ResponseHelper.forResponse(responseMono).toList(queryName, Book.class);
assertThat(books).hasSize(1);
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
}
static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
return initGraphQlSetup(null, null).queryFetcher(fieldName, fetcher);
}

View File

@@ -38,6 +38,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.graphql.BookSource;
import org.springframework.graphql.ExecutionGraphQlResponse;
import org.springframework.graphql.GraphQlSetup;
import org.springframework.graphql.ResponseHelper;
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
@@ -169,6 +170,25 @@ class QueryByExampleDataFetcherJpaTests {
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
}
@Test
void shouldNestForSingleArgumentInputType() {
Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"));
repository.saveAll(Arrays.asList(book1, book2));
String queryName = "booksByCriteria";
Mono<ExecutionGraphQlResponse> responseMono =
graphQlSetup(queryName, QueryByExampleDataFetcher.builder(repository).many())
.toGraphQlService()
.execute(request("{" + queryName + "(criteria: {id: 42}) {name}}"));
List<Book> books = ResponseHelper.forResponse(responseMono).toList(queryName, Book.class);
assertThat(books).hasSize(1);
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
}
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
return initGraphQlSetup(null).queryFetcher(fieldName, fetcher);
}