Querydsl and QBE nest into single argument input type
Closes gh-216
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user