diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 1523a079..e3308692 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -770,6 +770,53 @@ given a list of source/parent books objects. ==== +[[controllers-schema-mapping-argument-projections]] +==== Argument Projections + +When accessing individual arguments from a GraphQL request, interface projections can +be useful to access arguments through a well-defined interface. + +Spring Data's `@ProjectedPayload` can be used to annotate projection interfaces that +can be declared as handler method arguments. Payload projection can work on top-level +arguments (`DataFetchingEnvironment.getArguments()`). Alternatively, projections can +be applied on individual arguments by using `@Argument` with a projected payload interface. + +Argument projections are provided by https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces[Spring Data's Interface projections] +when Spring Data is on the class path. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Controller + public class BookController { + + @QueryMapping + public Book bookById(BookIdProjection bookId) { + // ... + } + + @MutationMapping + public Book addBook(@Argument BookInputProjection bookInput) { + // ... + } + } + + @ProjectedPayload + interface BookIdProjection { + + Long getId(); + } + + @ProjectedPayload + interface BookInputProjection { + + String getName(); + + @Value("#{target.author + ' ' + target.name}) + String getAuthorAndName(); + } +---- + + [[controllers-schema-mapping-data-loader]] ==== `DataLoader` diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java index f056895f..53eb11c6 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java @@ -84,6 +84,9 @@ public class AnnotatedControllerConfigurer */ private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; + private final static boolean springDataPresent = ClassUtils.isPresent( + "org.springframework.data.projection.SpelAwareProxyProjectionFactory", + AnnotatedControllerConfigurer.class.getClassLoader()); private final static boolean springSecurityPresent = ClassUtils.isPresent( "org.springframework.security.core.context.SecurityContext", AnnotatedControllerConfigurer.class.getClassLoader()); @@ -117,6 +120,9 @@ public class AnnotatedControllerConfigurer @Override public void afterPropertiesSet() { this.argumentResolvers = new HandlerMethodArgumentResolverComposite(); + if (springDataPresent) { + this.argumentResolvers.addResolver(new ProjectedPayloadMethodArgumentResolver(this.conversionService)); + } this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver()); this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(this.conversionService)); this.argumentResolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver()); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolver.java new file mode 100644 index 00000000..0e5d8def --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ProjectedPayloadMethodArgumentResolver.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2021 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 + * + * http://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.data.method.annotation.support; + + +import graphql.schema.DataFetchingEnvironment; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.web.ProjectedPayload; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.lang.Nullable; + +/** + * Resolver to obtain a {@link ProjectedPayload @ProjectedPayload} + * for {@link DataFetchingEnvironment#getArguments()}. + * + *
Projected payloads consist of the projection interface and accessor methods. + * Projections can be closed or open projections. Closed projections use interface + * getter methods to access underlying properties directly. Open projection methods + * make use of the {@code @Value} annotation to evaluate SpEL expressions against the + * underlying {@code target} object. + * + *
For example: + *
+ * @ProjectedPayload
+ * interface BookProjection {
+ * String getName();
+ * }
+ *
+ * @ProjectedPayload
+ * interface BookProjection {
+ * @Value("#{target.author + ' ' + target.name}")
+ * String getAuthorAndName();
+ * }
+ *
+ *
+ * @author Mark Paluch
+ * @since 1.0.0
+ */
+public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgumentResolver,
+ BeanFactoryAware, BeanClassLoaderAware {
+
+ private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
+
+ private final ArgumentMethodArgumentResolver argumentResolver;
+
+ public ProjectedPayloadMethodArgumentResolver(@Nullable ConversionService conversionService) {
+ this.argumentResolver = new ArgumentMethodArgumentResolver(conversionService){
+ @Override
+ protected Object convert(Object rawValue, Class> targetType) {
+ return project(targetType, rawValue);
+ }
+ };
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ Class> type = parameter.getParameterType();
+
+ if (!type.isInterface()) {
+ return false;
+ }
+
+ return AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null;
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
+ if(parameter.getParameterAnnotation(Argument.class) != null){
+ return argumentResolver.resolveArgument(parameter, environment);
+ }
+
+ return project(parameter.getParameterType(), environment.getArguments());
+ }
+
+ protected Object project(Class> projectionType, Object source){
+ return this.projectionFactory.createProjection(projectionType, source);
+ }
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.projectionFactory.setBeanFactory(beanFactory);
+ }
+
+ @Override
+ public void setBeanClassLoader(ClassLoader classLoader) {
+ this.projectionFactory.setBeanClassLoader(classLoader);
+ }
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java
index 74331899..fd3fb66f 100644
--- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java
+++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java
@@ -29,6 +29,7 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.web.ProjectedPayload;
import org.springframework.graphql.Author;
import org.springframework.graphql.Book;
import org.springframework.graphql.BookCriteria;
@@ -52,6 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Test GraphQL requests handled through {@code @SchemaMapping} methods.
*
* @author Rossen Stoyanchev
+ * @author Mark Paluch
*/
public class SchemaMappingInvocationTests {
@@ -96,6 +98,40 @@ public class SchemaMappingInvocationTests {
assertThat(bookList.get(1).getName()).isEqualTo("Animal Farm");
}
+ @Test
+ void queryWithProjectedArgument() {
+ String query = "{ " +
+ " booksByProjectedArguments(author:\"Orwell\") { " +
+ " id" +
+ " name" +
+ " }" +
+ "}";
+
+ Mono