Skip to content

Commit 38f1eab

Browse files
committed
Support batched EntityMapping methods
See gh-922
1 parent 23bf1fd commit 38f1eab

File tree

7 files changed

+387
-58
lines changed

7 files changed

+387
-58
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,33 +142,26 @@ public Object bind(
142142
Object rawValue = (name != null) ? environment.getArgument(name) : environment.getArguments();
143143
boolean isOmitted = (name != null && !environment.getArguments().containsKey(name));
144144

145-
return bind(name, rawValue, isOmitted, targetType);
145+
return bind(rawValue, isOmitted, targetType);
146146
}
147147

148148
/**
149149
* Variant of {@link #bind(DataFetchingEnvironment, String, ResolvableType)}
150150
* with a pre-extracted raw value to bind from.
151-
* @param name the name of an argument, or {@code null} to use the full map
152151
* @param rawValue the raw argument value (Collection, Map, or scalar)
153152
* @param isOmitted {@code true} if the argument was omitted from the input
154153
* and {@code false} if it was provided, but possibly {@code null}
155154
* @param targetType the type of Object to create
156155
* @since 1.3.0
157156
*/
158157
@Nullable
159-
public Object bind(
160-
@Nullable String name, @Nullable Object rawValue, boolean isOmitted, ResolvableType targetType)
161-
throws BindException {
162-
158+
public Object bind(@Nullable Object rawValue, boolean isOmitted, ResolvableType targetType) throws BindException {
163159
ArgumentsBindingResult bindingResult = new ArgumentsBindingResult(targetType);
164-
165-
Object value = bindRawValue(
166-
"$", rawValue, isOmitted, targetType, targetType.resolve(Object.class), bindingResult);
167-
160+
Class<?> targetClass = targetType.resolve(Object.class);
161+
Object value = bindRawValue("$", rawValue, isOmitted, targetType, targetClass, bindingResult);
168162
if (bindingResult.hasErrors()) {
169163
throw new BindException(bindingResult);
170164
}
171-
172165
return value;
173166
}
174167

spring-graphql/src/main/java/org/springframework/graphql/data/federation/EntitiesDataFetcher.java

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
2222
import java.util.Collections;
23+
import java.util.HashSet;
2324
import java.util.LinkedHashMap;
2425
import java.util.List;
2526
import java.util.Map;
27+
import java.util.Set;
2628
import java.util.concurrent.CompletionException;
2729

2830
import com.apollographql.federation.graphqljava._Entity;
@@ -38,6 +40,7 @@
3840
import org.springframework.graphql.data.method.annotation.support.HandlerDataFetcherExceptionResolver;
3941
import org.springframework.graphql.execution.ErrorType;
4042
import org.springframework.lang.Nullable;
43+
import org.springframework.util.Assert;
4144

4245
/**
4346
* DataFetcher that handles the "_entities" query by invoking
@@ -65,6 +68,7 @@ final class EntitiesDataFetcher implements DataFetcher<Mono<DataFetcherResult<Li
6568
public Mono<DataFetcherResult<List<Object>>> get(DataFetchingEnvironment environment) {
6669
List<Map<String, Object>> representations = environment.getArgument(_Entity.argumentName);
6770

71+
Set<String> batched = new HashSet<>();
6872
List<Mono<Object>> monoList = new ArrayList<>();
6973
for (int index = 0; index < representations.size(); index++) {
7074
Map<String, Object> map = representations.get(index);
@@ -79,15 +83,27 @@ public Mono<DataFetcherResult<List<Object>>> get(DataFetchingEnvironment environ
7983
monoList.add(resolveException(ex, environment, null, index));
8084
continue;
8185
}
82-
monoList.add(invokeResolver(environment, handlerMethod, map, index));
86+
87+
if (!handlerMethod.isBatchHandlerMethod()) {
88+
monoList.add(invokeEntityMethod(environment, handlerMethod, map, index));
89+
}
90+
else if (batched.contains(typename)) {
91+
// zip needs a value, this will be replaced by batch results
92+
monoList.add(Mono.just(Collections.emptyMap()));
93+
}
94+
else {
95+
EntityBatchDelegate batchDelegate = new EntityBatchDelegate(environment, handlerMethod, typename);
96+
monoList.add(batchDelegate.invokeEntityBatchMethod());
97+
batched.add(typename);
98+
}
8399
}
84100
return Mono.zip(monoList, Arrays::asList).map(EntitiesDataFetcher::toDataFetcherResult);
85101
}
86102

87-
private Mono<Object> invokeResolver(
103+
private Mono<Object> invokeEntityMethod(
88104
DataFetchingEnvironment env, EntityHandlerMethod handlerMethod, Map<String, Object> map, int index) {
89105

90-
return handlerMethod.getEntity(env, map, index)
106+
return handlerMethod.getEntity(env, map)
91107
.switchIfEmpty(Mono.error(new RepresentationNotResolvedException(map, handlerMethod)))
92108
.onErrorResume((ex) -> resolveException(ex, env, handlerMethod, index));
93109
}
@@ -96,7 +112,7 @@ private Mono<Object> resolveException(
96112
Throwable ex, DataFetchingEnvironment env, @Nullable EntityHandlerMethod handlerMethod, int index) {
97113

98114
Throwable theEx = (ex instanceof CompletionException) ? ex.getCause() : ex;
99-
DataFetchingEnvironment theEnv = new EntityDataFetchingEnvironment(env, index);
115+
DataFetchingEnvironment theEnv = new IndexedDataFetchingEnvironment(env, index);
100116
Object handler = (handlerMethod != null) ? handlerMethod.getBean() : null;
101117

102118
return this.exceptionResolver.resolveException(theEx, theEnv, handler)
@@ -120,6 +136,9 @@ private static DataFetcherResult<List<Object>> toDataFetcherResult(List<Object>
120136
List<GraphQLError> errors = new ArrayList<>();
121137
for (int i = 0; i < entities.size(); i++) {
122138
Object entity = entities.get(i);
139+
if (entity instanceof EntityBatchDelegate delegate) {
140+
delegate.processResults(entities, errors);
141+
}
123142
if (entity instanceof ErrorContainer errorContainer) {
124143
errors.addAll(errorContainer.errors());
125144
entities.set(i, null);
@@ -129,11 +148,80 @@ private static DataFetcherResult<List<Object>> toDataFetcherResult(List<Object>
129148
}
130149

131150

132-
private static class EntityDataFetchingEnvironment extends DelegatingDataFetchingEnvironment {
151+
private class EntityBatchDelegate {
152+
153+
private final DataFetchingEnvironment environment;
154+
155+
private final EntityHandlerMethod handlerMethod;
156+
157+
private final List<Map<String, Object>> representations = new ArrayList<>();
158+
159+
private final List<Integer> indexes = new ArrayList<>();
160+
161+
@Nullable
162+
private List<?> resultList;
163+
164+
EntityBatchDelegate(DataFetchingEnvironment env, EntityHandlerMethod handlerMethod, String typeName) {
165+
this.environment = env;
166+
this.handlerMethod = handlerMethod;
167+
List<Map<String, Object>> maps = env.getArgument(_Entity.argumentName);
168+
for (int i = 0; i < maps.size(); i++) {
169+
Map<String, Object> map = maps.get(i);
170+
if (typeName.equals(map.get("__typename"))) {
171+
this.representations.add(map);
172+
this.indexes.add(i);
173+
}
174+
}
175+
}
176+
177+
Mono<Object> invokeEntityBatchMethod() {
178+
return this.handlerMethod.getEntities(this.environment, this.representations)
179+
.mapNotNull((result) -> (((List<?>) result).isEmpty()) ? null : result)
180+
.switchIfEmpty(Mono.defer(this::handleEmptyResult))
181+
.onErrorResume(this::handleErrorResult)
182+
.map((result) -> {
183+
this.resultList = (List<?>) result;
184+
return this;
185+
});
186+
}
187+
188+
Mono<Object> handleEmptyResult() {
189+
List<Mono<Object>> exceptions = new ArrayList<>(this.indexes.size());
190+
for (int i = 0; i < this.indexes.size(); i++) {
191+
Map<String, Object> map = this.representations.get(i);
192+
Exception ex = new RepresentationNotResolvedException(map, this.handlerMethod);
193+
exceptions.add(resolveException(ex, this.environment, this.handlerMethod, this.indexes.get(i)));
194+
}
195+
return Mono.zip(exceptions, Arrays::asList);
196+
}
197+
198+
Mono<List<Object>> handleErrorResult(Throwable ex) {
199+
List<Mono<Object>> list = new ArrayList<>();
200+
for (Integer index : this.indexes) {
201+
list.add(resolveException(ex, this.environment, this.handlerMethod, index));
202+
}
203+
return Mono.zip(list, Arrays::asList);
204+
}
205+
206+
void processResults(List<Object> entities, List<GraphQLError> errors) {
207+
Assert.state(this.resultList != null, "Expected resultList");
208+
for (int i = 0; i < this.resultList.size(); i++) {
209+
Object entity = this.resultList.get(i);
210+
if (entity instanceof ErrorContainer errorContainer) {
211+
errors.addAll(errorContainer.errors());
212+
entity = null;
213+
}
214+
entities.set(this.indexes.get(i), entity);
215+
}
216+
}
217+
}
218+
219+
220+
private static class IndexedDataFetchingEnvironment extends DelegatingDataFetchingEnvironment {
133221

134222
private final ExecutionStepInfo executionStepInfo;
135223

136-
EntityDataFetchingEnvironment(DataFetchingEnvironment env, int index) {
224+
IndexedDataFetchingEnvironment(DataFetchingEnvironment env, int index) {
137225
super(env);
138226
this.executionStepInfo = ExecutionStepInfo.newExecutionStepInfo(env.getExecutionStepInfo())
139227
.path(env.getExecutionStepInfo().getPath().segment(index))

spring-graphql/src/main/java/org/springframework/graphql/data/federation/EntityArgumentMethodArgumentResolver.java

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.graphql.data.federation;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
1921
import java.util.Map;
2022

2123
import graphql.schema.DataFetchingEnvironment;
@@ -25,6 +27,7 @@
2527
import org.springframework.graphql.data.GraphQlArgumentBinder;
2628
import org.springframework.graphql.data.method.annotation.Argument;
2729
import org.springframework.graphql.data.method.annotation.support.ArgumentMethodArgumentResolver;
30+
import org.springframework.lang.Nullable;
2831
import org.springframework.validation.BindException;
2932

3033
/**
@@ -48,24 +51,52 @@ protected Object doBind(
4851
DataFetchingEnvironment environment, String name, ResolvableType targetType) throws BindException {
4952

5053
if (environment instanceof EntityDataFetchingEnvironment entityEnv) {
51-
Map<String, Object> entityMap = entityEnv.getRepresentation();
52-
Object rawValue = entityMap.get(name);
53-
boolean isOmitted = !entityMap.containsKey(name);
54-
return getArgumentBinder().bind(name, rawValue, isOmitted, targetType);
54+
return doBind(name, targetType, entityEnv.getRepresentation());
5555
}
56+
else if (environment instanceof EntityBatchDataFetchingEnvironment batchEnv) {
57+
name = dePluralize(name);
58+
targetType = targetType.getNested(2);
59+
List<Object> values = new ArrayList<>();
60+
for (Map<String, Object> representation : batchEnv.getRepresentations()) {
61+
values.add(doBind(name, targetType, representation));
62+
}
63+
return values;
64+
}
65+
else {
66+
throw new IllegalStateException("Expected decorated DataFetchingEnvironment");
67+
}
68+
}
69+
70+
@Nullable
71+
private Object doBind(String name, ResolvableType targetType, Map<String, Object> entityMap) throws BindException {
72+
Object rawValue = entityMap.get(name);
73+
boolean isOmitted = !entityMap.containsKey(name);
74+
return getArgumentBinder().bind(rawValue, isOmitted, targetType);
75+
}
5676

57-
throw new IllegalStateException("Expected decorated DataFetchingEnvironment");
77+
private static String dePluralize(String name) {
78+
return (name.endsWith("List")) ? name.substring(0, name.length() - 4) : name;
5879
}
5980

81+
6082
/**
61-
* Wrap the environment in order to also expose the entity representation map.
83+
* Utility method for use from {@link EntityHandlerMethod} to make the entity
84+
* representation map available.
6285
*/
6386
static DataFetchingEnvironment wrap(DataFetchingEnvironment env, Map<String, Object> representation) {
6487
return new EntityDataFetchingEnvironment(env, representation);
6588
}
6689

90+
/**
91+
* Utility method for use from {@link EntityHandlerMethod} to make the list
92+
* of entity representation maps available.
93+
*/
94+
static DataFetchingEnvironment wrap(DataFetchingEnvironment env, List<Map<String, Object>> representations) {
95+
return new EntityBatchDataFetchingEnvironment(env, representations);
96+
}
97+
6798

68-
private static class EntityDataFetchingEnvironment extends DelegatingDataFetchingEnvironment {
99+
static class EntityDataFetchingEnvironment extends DelegatingDataFetchingEnvironment {
69100

70101
private final Map<String, Object> representation;
71102

@@ -79,4 +110,19 @@ Map<String, Object> getRepresentation() {
79110
}
80111
}
81112

113+
114+
static class EntityBatchDataFetchingEnvironment extends DelegatingDataFetchingEnvironment {
115+
116+
private final List<Map<String, Object>> representations;
117+
118+
EntityBatchDataFetchingEnvironment(DataFetchingEnvironment env, List<Map<String, Object>> representations) {
119+
super(env);
120+
this.representations = representations;
121+
}
122+
123+
List<Map<String, Object>> getRepresentations() {
124+
return this.representations;
125+
}
126+
}
127+
82128
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2002-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.data.federation;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import graphql.schema.DataFetchingEnvironment;
23+
24+
import org.springframework.core.MethodParameter;
25+
import org.springframework.core.ResolvableType;
26+
import org.springframework.graphql.data.GraphQlArgumentBinder;
27+
import org.springframework.graphql.data.federation.EntityArgumentMethodArgumentResolver.EntityBatchDataFetchingEnvironment;
28+
import org.springframework.graphql.data.federation.EntityArgumentMethodArgumentResolver.EntityDataFetchingEnvironment;
29+
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* Resolver for the representation map of an entity, or for all representations
34+
* for the target schema type (batched handler methods).
35+
*
36+
* @author Rossen Stoyanchev
37+
*/
38+
final class EntityArgumentsMethodArgumentResolver implements HandlerMethodArgumentResolver {
39+
40+
private final GraphQlArgumentBinder argumentBinder;
41+
42+
43+
EntityArgumentsMethodArgumentResolver(GraphQlArgumentBinder argumentBinder) {
44+
Assert.notNull(argumentBinder, "GraphQlArgumentBinder is required");
45+
this.argumentBinder = argumentBinder;
46+
}
47+
48+
49+
@Override
50+
public boolean supportsParameter(MethodParameter param) {
51+
if (param.getParameterType().equals(List.class)) {
52+
param = param.nested(0);
53+
}
54+
if (param.getNestedParameterType().equals(Map.class)) {
55+
return param.nested(0).getNestedParameterType().equals(String.class);
56+
}
57+
return false;
58+
}
59+
60+
@Override
61+
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment env) throws Exception {
62+
ResolvableType targetType = ResolvableType.forMethodParameter(parameter);
63+
if (env instanceof EntityDataFetchingEnvironment entityEnv) {
64+
return this.argumentBinder.bind(entityEnv.getRepresentation(), false, targetType);
65+
}
66+
else if (env instanceof EntityBatchDataFetchingEnvironment batchEnv) {
67+
return this.argumentBinder.bind(batchEnv.getRepresentations(), false, targetType);
68+
}
69+
else {
70+
throw new IllegalStateException("Expected decorated DataFetchingEnvironment");
71+
}
72+
}
73+
74+
}

0 commit comments

Comments
 (0)