Skip to content

Commit b9075a7

Browse files
committed
Flatten nested argument maps in QuerydslDataFetcher.
We now flatten argument maps to ensure keys in the resulting parameter map are fully-qualified property paths. Previously, we built a parameter map containing nested maps leading to invalid queries.
1 parent 381c9f3 commit b9075a7

File tree

5 files changed

+116
-15
lines changed

5 files changed

+116
-15
lines changed

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,21 +142,34 @@ public String getDescription() {
142142
* @param environment contextual info for the GraphQL request
143143
* @return the resulting predicate
144144
*/
145-
@SuppressWarnings({"unchecked"})
146145
protected Predicate buildPredicate(DataFetchingEnvironment environment) {
147146
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
148147
QuerydslBindings bindings = new QuerydslBindings();
149148

150149
EntityPath<?> path = SimpleEntityPathResolver.INSTANCE.createPath(this.domainType.getType());
151150
this.customizer.customize(bindings, path);
152151

153-
for (Map.Entry<String, Object> entry : getArgumentValues(environment).entrySet()) {
152+
parameters.putAll(flatten(null, getArgumentValues(environment)));
153+
154+
return BUILDER.getPredicate(this.domainType, parameters, bindings);
155+
}
156+
157+
@SuppressWarnings("unchecked")
158+
private MultiValueMap<String, Object> flatten(@Nullable String prefix, Map<String, Object> inputParameters) {
159+
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
160+
161+
for (Map.Entry<String, Object> entry : inputParameters.entrySet()) {
154162
Object value = entry.getValue();
155-
List<Object> values = (value instanceof List) ? (List<Object>) value : Collections.singletonList(value);
156-
parameters.put(entry.getKey(), values);
163+
if (value instanceof Map<?, ?> nested) {
164+
parameters.addAll(flatten(entry.getKey(), (Map<String, Object>) nested));
165+
}
166+
else {
167+
List<Object> values = (value instanceof List) ? (List<Object>) value : Collections.singletonList(value);
168+
parameters.put(((prefix != null) ? prefix + "." : "") + entry.getKey(), values);
169+
}
157170
}
158171

159-
return BUILDER.getPredicate(this.domainType, parameters, bindings);
172+
return parameters;
160173
}
161174

162175
/**
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql;
18+
19+
import com.querydsl.core.types.Path;
20+
import com.querydsl.core.types.PathMetadata;
21+
import com.querydsl.core.types.dsl.EntityPathBase;
22+
import com.querydsl.core.types.dsl.NumberPath;
23+
import com.querydsl.core.types.dsl.StringPath;
24+
25+
import static com.querydsl.core.types.PathMetadataFactory.forVariable;
26+
27+
/**
28+
* QAuthor is a Querydsl query type for Author
29+
*/
30+
public class QAuthor extends EntityPathBase<Author> {
31+
private static final long serialVersionUID = 1773522017L;
32+
public static final QAuthor author = new QAuthor("author");
33+
public final StringPath firstName = createString("firstName");
34+
public final NumberPath<Long> id = createNumber("id", Long.class);
35+
public final StringPath lastName = createString("lastName");
36+
37+
public QAuthor(String variable) {
38+
super(Author.class, forVariable(variable));
39+
}
40+
41+
public QAuthor(Path<? extends Author> path) {
42+
super(path.getType(), path.getMetadata());
43+
}
44+
45+
public QAuthor(PathMetadata metadata) {
46+
super(Author.class, metadata);
47+
}
48+
49+
}

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,43 @@
1818

1919
import com.querydsl.core.types.Path;
2020
import com.querydsl.core.types.PathMetadata;
21-
import com.querydsl.core.types.PathMetadataFactory;
2221
import com.querydsl.core.types.dsl.EntityPathBase;
2322
import com.querydsl.core.types.dsl.NumberPath;
23+
import com.querydsl.core.types.dsl.PathInits;
2424
import com.querydsl.core.types.dsl.StringPath;
2525

26+
import static com.querydsl.core.types.PathMetadataFactory.forVariable;
27+
2628
/**
27-
* Generated by Querydsl.
29+
* QBook is a Querydsl query type for Book
2830
*/
2931
public class QBook extends EntityPathBase<Book> {
3032
private static final long serialVersionUID = 1773522017L;
33+
private static final PathInits INITS = PathInits.DIRECT2;
3134
public static final QBook book = new QBook("book");
32-
public final StringPath author = this.createString("author");
33-
public final NumberPath<Long> id = this.createNumber("id", Long.class);
34-
public final StringPath name = this.createString("name");
35+
public final org.springframework.graphql.QAuthor author;
36+
public final NumberPath<Long> id = createNumber("id", Long.class);
37+
public final StringPath name = createString("name");
3538

3639
public QBook(String variable) {
37-
super(Book.class, PathMetadataFactory.forVariable(variable));
40+
this(Book.class, forVariable(variable), INITS);
3841
}
3942

4043
public QBook(Path<? extends Book> path) {
41-
super(path.getType(), path.getMetadata());
44+
this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
4245
}
4346

4447
public QBook(PathMetadata metadata) {
45-
super(Book.class, metadata);
48+
this(metadata, PathInits.getFor(metadata, INITS));
49+
}
50+
51+
public QBook(PathMetadata metadata, PathInits inits) {
52+
this(Book.class, metadata, inits);
4653
}
54+
55+
public QBook(Class<? extends Book> type, PathMetadata metadata, PathInits inits) {
56+
super(type, metadata, inits);
57+
this.author = inits.isInitialized("author") ? new org.springframework.graphql.QAuthor(forProperty("author")) : null;
58+
}
59+
4760
}

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,14 @@ void shouldApplyCustomizerViaBuilder() {
215215
.many();
216216

217217
graphQlSetup("books", fetcher).toWebGraphQlHandler()
218-
.handleRequest(request("{ books(name: \"H\", author: \"Doug\") {name}}"))
218+
.handleRequest(request("{ books(name: \"H\") {name}}"))
219219
.block();
220220

221221
ArgumentCaptor<Predicate> predicateCaptor = ArgumentCaptor.forClass(Predicate.class);
222222
verify(mockRepository).findBy(predicateCaptor.capture(), any());
223223

224224
Predicate predicate = predicateCaptor.getValue();
225-
assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H").and(QBook.book.author.eq("Doug")));
225+
assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H"));
226226
}
227227

228228
@Test
@@ -346,6 +346,25 @@ void shouldNestForSingleArgumentInputType() {
346346
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
347347
}
348348

349+
@Test
350+
void shouldConsiderNestedArguments() {
351+
Book book1 = new Book(42L, "Hitchhiker's Guide to the Galaxy", new Author(0L, "Douglas", "Adams"));
352+
Book book2 = new Book(53L, "Breaking Bad", new Author(0L, "", "Heisenberg"));
353+
mockRepository.saveAll(Arrays.asList(book1, book2));
354+
355+
String queryName = "booksByNestableCriteria";
356+
357+
Mono<ExecutionGraphQlResponse> responseMono =
358+
graphQlSetup(queryName, QuerydslDataFetcher.builder(mockRepository).many())
359+
.toGraphQlService()
360+
.execute(request("{" + queryName + "(author: {firstName: \"Douglas\"}) {name}}"));
361+
362+
List<Book> books = ResponseHelper.forResponse(responseMono).toList(queryName, Book.class);
363+
364+
assertThat(books).hasSize(1);
365+
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
366+
}
367+
349368
private static GraphQlSetup graphQlSetup(String fieldName, DataFetcher<?> fetcher) {
350369
return GraphQlSetup.schemaResource(BookSource.schema).queryFetcher(fieldName, fetcher);
351370
}

spring-graphql/src/test/resources/books/schema.graphqls

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ type Query {
22
bookById(id: ID): Book
33
booksById(id: [ID]): [Book]
44
books(id: ID, name: String, author: String): [Book!]!
5+
booksByNestableCriteria(id: ID, name: String, author: AuthorCriteria): [Book!]!
56
booksByCriteria(criteria:BookCriteria): [Book]
67
booksByProjectedArguments(name: String, author: String): [Book]
78
booksByProjectedCriteria(criteria:BookCriteria): [Book]
@@ -21,6 +22,12 @@ input BookCriteria {
2122
author: String
2223
}
2324

25+
input AuthorCriteria {
26+
id: ID
27+
firstName: String
28+
lastName: String
29+
}
30+
2431
type Book {
2532
id: ID
2633
name: String

0 commit comments

Comments
 (0)