diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java index 53a84f4f3..7d0c58525 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/query/QuerydslDataFetcher.java @@ -142,7 +142,6 @@ public String getDescription() { * @param environment contextual info for the GraphQL request * @return the resulting predicate */ - @SuppressWarnings({"unchecked"}) protected Predicate buildPredicate(DataFetchingEnvironment environment) { MultiValueMap parameters = new LinkedMultiValueMap<>(); QuerydslBindings bindings = new QuerydslBindings(); @@ -150,13 +149,27 @@ protected Predicate buildPredicate(DataFetchingEnvironment environment) { EntityPath path = SimpleEntityPathResolver.INSTANCE.createPath(this.domainType.getType()); this.customizer.customize(bindings, path); - for (Map.Entry entry : getArgumentValues(environment).entrySet()) { + parameters.putAll(flatten(null, getArgumentValues(environment))); + + return BUILDER.getPredicate(this.domainType, parameters, bindings); + } + + @SuppressWarnings("unchecked") + private MultiValueMap flatten(@Nullable String prefix, Map inputParameters) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + + for (Map.Entry entry : inputParameters.entrySet()) { Object value = entry.getValue(); - List values = (value instanceof List) ? (List) value : Collections.singletonList(value); - parameters.put(entry.getKey(), values); + if (value instanceof Map nested) { + parameters.addAll(flatten(entry.getKey(), (Map) nested)); + } + else { + List values = (value instanceof List) ? (List) value : Collections.singletonList(value); + parameters.put(((prefix != null) ? prefix + "." : "") + entry.getKey(), values); + } } - return BUILDER.getPredicate(this.domainType, parameters, bindings); + return parameters; } /** diff --git a/spring-graphql/src/test/java/org/springframework/graphql/QAuthor.java b/spring-graphql/src/test/java/org/springframework/graphql/QAuthor.java new file mode 100644 index 000000000..3d7fc6dfa --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/QAuthor.java @@ -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 { + private static final long serialVersionUID = 1773522017L; + public static final QAuthor author = new QAuthor("author"); + public final StringPath firstName = createString("firstName"); + public final NumberPath id = createNumber("id", Long.class); + public final StringPath lastName = createString("lastName"); + + public QAuthor(String variable) { + super(Author.class, forVariable(variable)); + } + + public QAuthor(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QAuthor(PathMetadata metadata) { + super(Author.class, metadata); + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QBook.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QBook.java index 1f6bbbbc4..fd2af98f8 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QBook.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QBook.java @@ -18,30 +18,43 @@ 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 { 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 id = this.createNumber("id", Long.class); - public final StringPath name = this.createString("name"); + public final org.springframework.graphql.QAuthor author; + public final NumberPath 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 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 type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.author = inits.isInitialized("author") ? new org.springframework.graphql.QAuthor(forProperty("author")) : null; + } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java index 70b8b2acb..e3484f5f9 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/query/QuerydslDataFetcherTests.java @@ -215,14 +215,14 @@ void shouldApplyCustomizerViaBuilder() { .many(); graphQlSetup("books", fetcher).toWebGraphQlHandler() - .handleRequest(request("{ books(name: \"H\", author: \"Doug\") {name}}")) + .handleRequest(request("{ books(name: \"H\") {name}}")) .block(); ArgumentCaptor 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 @@ void shouldNestForSingleArgumentInputType() { 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 responseMono = + graphQlSetup(queryName, QuerydslDataFetcher.builder(mockRepository).many()) + .toGraphQlService() + .execute(request("{" + queryName + "(author: {firstName: \"Douglas\"}) {name}}")); + + List 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); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java index 6b1f42472..e5c1546d9 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/observation/GraphQlObservationInstrumentationTests.java @@ -343,12 +343,14 @@ void shouldRecordGraphQlErrorsAsTraceEvents() { assertThat(response.errorCount()).isEqualTo(1); assertThat(response.error(0).errorType()).isEqualTo("InvalidSyntax"); - assertThat(response.error(0).message()).startsWith("Invalid syntax with offending token 'invalid'"); + assertThat(response.error(0).message()).containsIgnoringCase("syntax") + .containsIgnoringCase("token").contains("'invalid'"); assertThat(observationHandler.getEvents()).hasSize(1); Observation.Event errorEvent = observationHandler.getEvents().get(0); assertThat(errorEvent.getName()).isEqualTo("InvalidSyntax"); - assertThat(errorEvent.getContextualName()).startsWith("Invalid syntax with offending token 'invalid'"); + assertThat(errorEvent.getContextualName()).containsIgnoringCase("syntax") + .containsIgnoringCase("token").contains("'invalid'"); TestObservationRegistryAssert.assertThat(this.observationRegistry).hasObservationWithNameEqualTo("graphql.request") .that().hasLowCardinalityKeyValue("graphql.outcome", "REQUEST_ERROR") diff --git a/spring-graphql/src/test/resources/books/schema.graphqls b/spring-graphql/src/test/resources/books/schema.graphqls index e1d7bcddb..9501ce69a 100644 --- a/spring-graphql/src/test/resources/books/schema.graphqls +++ b/spring-graphql/src/test/resources/books/schema.graphqls @@ -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