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:
Mark Paluch
2024-11-08 11:23:35 +01:00
committed by rstoyanchev
parent 381c9f3cc0
commit 821d480c3a
5 changed files with 117 additions and 16 deletions

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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