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
This commit is contained in:
@@ -142,7 +142,6 @@ public abstract class QuerydslDataFetcher<T> {
|
||||
* @param environment contextual info for the GraphQL request
|
||||
* @return the resulting predicate
|
||||
*/
|
||||
@SuppressWarnings({"unchecked"})
|
||||
protected Predicate buildPredicate(DataFetchingEnvironment environment) {
|
||||
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
QuerydslBindings bindings = new QuerydslBindings();
|
||||
@@ -150,15 +149,29 @@ public abstract class QuerydslDataFetcher<T> {
|
||||
EntityPath<?> path = SimpleEntityPathResolver.INSTANCE.createPath(this.domainType.getType());
|
||||
this.customizer.customize(bindings, path);
|
||||
|
||||
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);
|
||||
}
|
||||
parameters.putAll(flatten(null, getArgumentValues(environment)));
|
||||
|
||||
return BUILDER.getPredicate(this.domainType, parameters, bindings);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private MultiValueMap<String, Object> flatten(@Nullable String prefix, Map<String, Object> inputParameters) {
|
||||
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
|
||||
for (Map.Entry<String, Object> entry : inputParameters.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Map<?, ?> nested) {
|
||||
parameters.addAll(flatten(entry.getKey(), (Map<String, Object>) nested));
|
||||
}
|
||||
else {
|
||||
List<Object> values = (value instanceof List) ? (List<Object>) value : Collections.singletonList(value);
|
||||
parameters.put(((prefix != null) ? prefix + "." : "") + entry.getKey(), values);
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql;
|
||||
|
||||
import com.querydsl.core.types.Path;
|
||||
import com.querydsl.core.types.PathMetadata;
|
||||
import com.querydsl.core.types.dsl.EntityPathBase;
|
||||
import com.querydsl.core.types.dsl.NumberPath;
|
||||
import com.querydsl.core.types.dsl.StringPath;
|
||||
|
||||
import static com.querydsl.core.types.PathMetadataFactory.forVariable;
|
||||
|
||||
/**
|
||||
* QAuthor is a Querydsl query type for Author
|
||||
*/
|
||||
public class QAuthor extends EntityPathBase<Author> {
|
||||
private static final long serialVersionUID = 1773522017L;
|
||||
public static final QAuthor author = new QAuthor("author");
|
||||
public final StringPath firstName = createString("firstName");
|
||||
public final NumberPath<Long> id = createNumber("id", Long.class);
|
||||
public final StringPath lastName = createString("lastName");
|
||||
|
||||
public QAuthor(String variable) {
|
||||
super(Author.class, forVariable(variable));
|
||||
}
|
||||
|
||||
public QAuthor(Path<? extends Author> path) {
|
||||
super(path.getType(), path.getMetadata());
|
||||
}
|
||||
|
||||
public QAuthor(PathMetadata metadata) {
|
||||
super(Author.class, metadata);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,30 +18,43 @@ package org.springframework.graphql.data.query;
|
||||
|
||||
import com.querydsl.core.types.Path;
|
||||
import com.querydsl.core.types.PathMetadata;
|
||||
import com.querydsl.core.types.PathMetadataFactory;
|
||||
import com.querydsl.core.types.dsl.EntityPathBase;
|
||||
import com.querydsl.core.types.dsl.NumberPath;
|
||||
import com.querydsl.core.types.dsl.PathInits;
|
||||
import com.querydsl.core.types.dsl.StringPath;
|
||||
|
||||
import static com.querydsl.core.types.PathMetadataFactory.forVariable;
|
||||
|
||||
/**
|
||||
* Generated by Querydsl.
|
||||
* QBook is a Querydsl query type for Book
|
||||
*/
|
||||
public class QBook extends EntityPathBase<Book> {
|
||||
private static final long serialVersionUID = 1773522017L;
|
||||
private static final PathInits INITS = PathInits.DIRECT2;
|
||||
public static final QBook book = new QBook("book");
|
||||
public final StringPath author = this.createString("author");
|
||||
public final NumberPath<Long> id = this.createNumber("id", Long.class);
|
||||
public final StringPath name = this.createString("name");
|
||||
public final org.springframework.graphql.QAuthor author;
|
||||
public final NumberPath<Long> id = createNumber("id", Long.class);
|
||||
public final StringPath name = createString("name");
|
||||
|
||||
public QBook(String variable) {
|
||||
super(Book.class, PathMetadataFactory.forVariable(variable));
|
||||
this(Book.class, forVariable(variable), INITS);
|
||||
}
|
||||
|
||||
public QBook(Path<? extends Book> path) {
|
||||
super(path.getType(), path.getMetadata());
|
||||
this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
|
||||
}
|
||||
|
||||
public QBook(PathMetadata metadata) {
|
||||
super(Book.class, metadata);
|
||||
this(metadata, PathInits.getFor(metadata, INITS));
|
||||
}
|
||||
|
||||
public QBook(PathMetadata metadata, PathInits inits) {
|
||||
this(Book.class, metadata, inits);
|
||||
}
|
||||
|
||||
public QBook(Class<? extends Book> type, PathMetadata metadata, PathInits inits) {
|
||||
super(type, metadata, inits);
|
||||
this.author = inits.isInitialized("author") ? new org.springframework.graphql.QAuthor(forProperty("author")) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -215,14 +215,14 @@ class QuerydslDataFetcherTests {
|
||||
.many();
|
||||
|
||||
graphQlSetup("books", fetcher).toWebGraphQlHandler()
|
||||
.handleRequest(request("{ books(name: \"H\", author: \"Doug\") {name}}"))
|
||||
.handleRequest(request("{ books(name: \"H\") {name}}"))
|
||||
.block();
|
||||
|
||||
ArgumentCaptor<Predicate> predicateCaptor = ArgumentCaptor.forClass(Predicate.class);
|
||||
verify(mockRepository).findBy(predicateCaptor.capture(), any());
|
||||
|
||||
Predicate predicate = predicateCaptor.getValue();
|
||||
assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H").and(QBook.book.author.eq("Doug")));
|
||||
assertThat(predicate).isEqualTo(QBook.book.name.startsWith("H"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -346,6 +346,25 @@ class QuerydslDataFetcherTests {
|
||||
assertThat(books.get(0).getName()).isEqualTo(book1.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConsiderNestedArguments() {
|
||||
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 = "booksByNestableCriteria";
|
||||
|
||||
Mono<ExecutionGraphQlResponse> responseMono =
|
||||
graphQlSetup(queryName, QuerydslDataFetcher.builder(mockRepository).many())
|
||||
.toGraphQlService()
|
||||
.execute(request("{" + queryName + "(author: {firstName: \"Douglas\"}) {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 GraphQlSetup.schemaResource(BookSource.schema).queryFetcher(fieldName, fetcher);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ type Query {
|
||||
bookById(id: ID): Book
|
||||
booksById(id: [ID]): [Book]
|
||||
books(id: ID, name: String, author: String): [Book!]!
|
||||
booksByNestableCriteria(id: ID, name: String, author: AuthorCriteria): [Book!]!
|
||||
booksByCriteria(criteria:BookCriteria): [Book]
|
||||
booksByProjectedArguments(name: String, author: String): [Book]
|
||||
booksByProjectedCriteria(criteria:BookCriteria): [Book]
|
||||
@@ -21,6 +22,12 @@ input BookCriteria {
|
||||
author: String
|
||||
}
|
||||
|
||||
input AuthorCriteria {
|
||||
id: ID
|
||||
firstName: String
|
||||
lastName: String
|
||||
}
|
||||
|
||||
type Book {
|
||||
id: ID
|
||||
name: String
|
||||
|
||||
Reference in New Issue
Block a user