Skip to content

Commit 821d480

Browse files
mp911derstoyanchev
authored andcommitted
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. Closes gh-1085
1 parent 381c9f3 commit 821d480

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

+18-5
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
/**
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

+21-8
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

+21-2
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

+7
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)