Skip to content

Commit 911b782

Browse files
committed
Querydsl and QBE nest into single argument input type
Closes gh-216
1 parent 4ae21a8 commit 911b782

File tree

6 files changed

+96
-13
lines changed

6 files changed

+96
-13
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -865,9 +865,11 @@ Then use it to create a `DataFetcher`:
865865
You can now register the above `DataFetcher` through a
866866
<<execution.graphqlsource.runtimewiring-configurer>>.
867867

868-
The `DataFetcher` builds a Querydsl `Predicate` from GraphQL request parameters, and
869-
uses it to fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA,
870-
MongoDB, and LDAP.
868+
The `DataFetcher` builds a Querydsl `Predicate` from GraphQL arguments, and uses it to
869+
fetch data. Spring Data supports `QuerydslPredicateExecutor` for JPA, MongoDB, and LDAP.
870+
871+
NOTE: For a single argument that is a GraphQL input type, `QuerydslDataFetcher` nests one
872+
level down, and uses the values from the argument sub-map.
871873

872874
If the repository is `ReactiveQuerydslPredicateExecutor`, the builder returns
873875
`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
10581060
repository and use that as the example object to fetch data with. Spring Data supports
10591061
`QueryByExampleDataFetcher` for JPA, MongoDB, Neo4j, and Redis.
10601062

1063+
NOTE: For a single argument that is a GraphQL input type, `QueryByExampleDataFetcher`
1064+
nests one level down, and binds with the values from the argument sub-map.
1065+
10611066
If the repository is `ReactiveQueryByExampleExecutor`, the builder returns
10621067
`DataFetcher<Mono<Account>>` or `DataFetcher<Flux<Account>>`. Spring Data supports this
10631068
variant for MongoDB, Neo4j, Redis, and R2dbc.

spring-graphql/src/main/java/org/springframework/graphql/data/query/QueryByExampleDataFetcher.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import graphql.schema.DataFetcher;
2727
import graphql.schema.DataFetchingEnvironment;
2828
import graphql.schema.DataFetchingFieldSelectionSet;
29+
import graphql.schema.GraphQLArgument;
2930
import graphql.schema.GraphQLTypeVisitor;
3031
import org.apache.commons.logging.Log;
3132
import org.apache.commons.logging.LogFactory;
@@ -41,8 +42,9 @@
4142
import org.springframework.data.util.TypeInformation;
4243
import org.springframework.graphql.data.GraphQlArgumentBinder;
4344
import org.springframework.graphql.data.GraphQlRepository;
44-
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
4545
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
46+
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
47+
import org.springframework.lang.Nullable;
4648
import org.springframework.util.Assert;
4749
import org.springframework.validation.BindException;
4850

@@ -108,13 +110,32 @@ public abstract class QueryByExampleDataFetcher<T> {
108110

109111
/**
110112
* Prepare an {@link Example} from GraphQL request arguments.
111-
* @param env contextual info for the GraphQL request
113+
* @param environment contextual info for the GraphQL request
112114
* @return the resulting example
113115
*/
114116
@SuppressWarnings({"ConstantConditions", "unchecked"})
115-
protected Example<T> buildExample(DataFetchingEnvironment env) throws BindException {
117+
protected Example<T> buildExample(DataFetchingEnvironment environment) throws BindException {
118+
String name = getArgumentName(environment);
116119
ResolvableType targetType = ResolvableType.forClass(this.domainType.getType());
117-
return (Example<T>) Example.of(this.argumentBinder.bind(env, null, targetType));
120+
return (Example<T>) Example.of(this.argumentBinder.bind(environment, name, targetType));
121+
}
122+
123+
/**
124+
* For a single argument that is a GraphQL input type, return the argument
125+
* name, thereby nesting and having the example Object populated from the
126+
* sub-map. Otherwise, {@code null} to bind using the top-level map.
127+
*/
128+
@Nullable
129+
private static String getArgumentName(DataFetchingEnvironment environment) {
130+
Map<String, Object> arguments = environment.getArguments();
131+
List<GraphQLArgument> definedArguments = environment.getFieldDefinition().getArguments();
132+
if (definedArguments.size() == 1) {
133+
String name = definedArguments.get(0).getName();
134+
if (arguments.get(name) instanceof Map<?,?>) {
135+
return name;
136+
}
137+
}
138+
return null;
118139
}
119140

120141
protected boolean requiresProjection(Class<?> resultType) {

spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -47,8 +47,8 @@
4747
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
4848
import org.springframework.data.util.TypeInformation;
4949
import org.springframework.graphql.data.GraphQlRepository;
50-
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
5150
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
51+
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
5252
import org.springframework.util.Assert;
5353
import org.springframework.util.LinkedMultiValueMap;
5454
import org.springframework.util.MultiValueMap;
@@ -134,7 +134,7 @@ protected Predicate buildPredicate(DataFetchingEnvironment environment) {
134134
EntityPath<?> path = SimpleEntityPathResolver.INSTANCE.createPath(this.domainType.getType());
135135
this.customizer.customize(bindings, path);
136136

137-
for (Map.Entry<String, Object> entry : environment.getArguments().entrySet()) {
137+
for (Map.Entry<String, Object> entry : getArgumentValues(environment).entrySet()) {
138138
Object value = entry.getValue();
139139
List<Object> values = (value instanceof List ? (List<Object>) value : Collections.singletonList(value));
140140
parameters.put(entry.getKey(), values);
@@ -143,6 +143,23 @@ protected Predicate buildPredicate(DataFetchingEnvironment environment) {
143143
return BUILDER.getPredicate(this.domainType, parameters, bindings);
144144
}
145145

146+
/**
147+
* For a single argument that is a GraphQL input type, return the sub-map
148+
* under the argument name, or otherwise the top-level argument map.
149+
*/
150+
@SuppressWarnings("unchecked")
151+
private static Map<String, Object> getArgumentValues(DataFetchingEnvironment environment) {
152+
Map<String, Object> arguments = environment.getArguments();
153+
if (environment.getFieldDefinition().getArguments().size() == 1) {
154+
String name = environment.getFieldDefinition().getArguments().get(0).getName();
155+
Object value = arguments.get(name);
156+
if (value instanceof Map<?,?>) {
157+
return (Map<String, Object>) value;
158+
}
159+
}
160+
return arguments;
161+
}
162+
146163
protected boolean requiresProjection(Class<?> resultType) {
147164
return !resultType.equals(this.domainType.getType());
148165
}

spring-graphql/src/test/java/org/springframework/graphql/ResponseHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public <T> T rawValue(String path) {
106106

107107
private void assertNoErrors() {
108108
if (!this.errorsChecked) {
109-
assertThat(this.errors).as("Errors present in GraphQL response").isEmpty();
109+
assertThat(this.errors).as("GraphQL response errors: " + this.errors).isEmpty();
110110
this.errorsChecked = true;
111111
}
112112
}

spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.data.repository.Repository;
4545
import org.springframework.graphql.Author;
4646
import org.springframework.graphql.BookSource;
47+
import org.springframework.graphql.ExecutionGraphQlResponse;
4748
import org.springframework.graphql.GraphQlSetup;
4849
import org.springframework.graphql.ResponseHelper;
4950
import org.springframework.graphql.data.GraphQlRepository;
@@ -74,7 +75,7 @@ class QuerydslDataFetcherTests {
7475

7576

7677
@Test
77-
void shouldFetchSingleItems() {
78+
void shouldFetchSingleItem() {
7879
Book book = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
7980
mockRepository.save(book);
8081

@@ -277,6 +278,25 @@ void shouldReactivelyFetchMultipleItems() {
277278
tester.accept(graphQlSetup(mockRepository));
278279
}
279280

281+
@Test
282+
void shouldNestForSingleArgumentInputType() {
283+
Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
284+
Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"));
285+
mockRepository.saveAll(Arrays.asList(book1, book2));
286+
287+
String queryName = "booksByCriteria";
288+
289+
Mono<ExecutionGraphQlResponse> responseMono =
290+
graphQlSetup(queryName, QuerydslDataFetcher.builder(mockRepository).many())
291+
.toGraphQlService()
292+
.execute(request("{" + queryName + "(criteria: {id: 42}) {name}}"));
293+
294+
List<Book> books = ResponseHelper.forResponse(responseMono).toList(queryName, Book.class);
295+
296+
assertThat(books).hasSize(1);
297+
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
298+
}
299+
280300
static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
281301
return initGraphQlSetup(null, null).queryFetcher(fieldName, fetcher);
282302
}

spring-graphql/src/test/java/org/springframework/graphql/data/query/jpa/QueryByExampleDataFetcherJpaTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
3939
import org.springframework.data.repository.query.QueryByExampleExecutor;
4040
import org.springframework.graphql.BookSource;
41+
import org.springframework.graphql.ExecutionGraphQlResponse;
4142
import org.springframework.graphql.GraphQlSetup;
4243
import org.springframework.graphql.ResponseHelper;
4344
import org.springframework.graphql.data.query.QueryByExampleDataFetcher;
@@ -169,6 +170,25 @@ void shouldFetchSingleItemsWithDtoProjection() {
169170
assertThat(actualBook.getName()).isEqualTo("The book is: Hitchhiker's Guide to the Galaxy");
170171
}
171172

173+
@Test
174+
void shouldNestForSingleArgumentInputType() {
175+
Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
176+
Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"));
177+
repository.saveAll(Arrays.asList(book1, book2));
178+
179+
String queryName = "booksByCriteria";
180+
181+
Mono<ExecutionGraphQlResponse> responseMono =
182+
graphQlSetup(queryName, QueryByExampleDataFetcher.builder(repository).many())
183+
.toGraphQlService()
184+
.execute(request("{" + queryName + "(criteria: {id: 42}) {name}}"));
185+
186+
List<Book> books = ResponseHelper.forResponse(responseMono).toList(queryName, Book.class);
187+
188+
assertThat(books).hasSize(1);
189+
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
190+
}
191+
172192
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
173193
return initGraphQlSetup(null).queryFetcher(fieldName, fetcher);
174194
}

0 commit comments

Comments
 (0)