-
Notifications
You must be signed in to change notification settings - Fork 313
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
16 changed files
with
914 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
...raphql/src/main/java/org/springframework/graphql/data/federation/EntitiesDataFetcher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
/* | ||
* Copyright 2002-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.data.federation; | ||
|
||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.CompletionException; | ||
|
||
import com.apollographql.federation.graphqljava._Entity; | ||
import graphql.GraphQLError; | ||
import graphql.GraphqlErrorBuilder; | ||
import graphql.execution.DataFetcherResult; | ||
import graphql.execution.ExecutionStepInfo; | ||
import graphql.schema.DataFetcher; | ||
import graphql.schema.DataFetchingEnvironment; | ||
import graphql.schema.DelegatingDataFetchingEnvironment; | ||
import reactor.core.publisher.Mono; | ||
|
||
import org.springframework.graphql.data.method.annotation.support.HandlerDataFetcherExceptionResolver; | ||
import org.springframework.graphql.execution.ErrorType; | ||
import org.springframework.lang.Nullable; | ||
|
||
/** | ||
* DataFetcher that handles the "_entities" query by invoking | ||
* {@link EntityHandlerMethod}s. | ||
* | ||
* @author Rossen Stoyanchev | ||
* @since 1.3 | ||
* @see com.apollographql.federation.graphqljava.SchemaTransformer#fetchEntities(DataFetcher) | ||
*/ | ||
final class EntitiesDataFetcher implements DataFetcher<Mono<DataFetcherResult<List<Object>>>> { | ||
|
||
private final Map<String, EntityHandlerMethod> handlerMethods; | ||
|
||
private final HandlerDataFetcherExceptionResolver exceptionResolver; | ||
|
||
|
||
public EntitiesDataFetcher( | ||
Map<String, EntityHandlerMethod> handlerMethods, HandlerDataFetcherExceptionResolver resolver) { | ||
|
||
this.handlerMethods = new LinkedHashMap<>(handlerMethods); | ||
this.exceptionResolver = resolver; | ||
} | ||
|
||
|
||
@Override | ||
public Mono<DataFetcherResult<List<Object>>> get(DataFetchingEnvironment environment) { | ||
List<Map<String, Object>> representations = environment.getArgument(_Entity.argumentName); | ||
|
||
List<Mono<Object>> monoList = new ArrayList<>(); | ||
for (int index = 0; index < representations.size(); index++) { | ||
Map<String, Object> map = representations.get(index); | ||
if (!(map.get("__typename") instanceof String typename)) { | ||
Exception ex = new RepresentationException(map, "Missing \"__typename\" argument"); | ||
monoList.add(resolveException(ex, environment, null, index)); | ||
continue; | ||
} | ||
EntityHandlerMethod handlerMethod = this.handlerMethods.get(typename); | ||
if (handlerMethod == null) { | ||
Exception ex = new RepresentationException(map, "No entity fetcher"); | ||
monoList.add(resolveException(ex, environment, null, index)); | ||
continue; | ||
} | ||
monoList.add(invokeResolver(environment, handlerMethod, map, index)); | ||
} | ||
return Mono.zip(monoList, Arrays::asList).map(EntitiesDataFetcher::toDataFetcherResult); | ||
} | ||
|
||
private Mono<Object> invokeResolver( | ||
DataFetchingEnvironment env, EntityHandlerMethod handlerMethod, Map<String, Object> map, int index) { | ||
|
||
return handlerMethod.getEntity(env, map, index) | ||
.switchIfEmpty(Mono.error(new RepresentationNotResolvedException(map, handlerMethod))) | ||
.onErrorResume(ex -> resolveException(ex, env, handlerMethod, index)); | ||
} | ||
|
||
private Mono<Object> resolveException( | ||
Throwable ex, DataFetchingEnvironment env, @Nullable EntityHandlerMethod handlerMethod, int index) { | ||
|
||
Throwable theEx = (ex instanceof CompletionException ? ex.getCause() : ex); | ||
DataFetchingEnvironment theEnv = new EntityDataFetchingEnvironment(env, index); | ||
Object handler = (handlerMethod != null ? handlerMethod.getBean() : null); | ||
|
||
return this.exceptionResolver.resolveException(theEx, theEnv, handler) | ||
.map(ErrorContainer::new) | ||
.switchIfEmpty(Mono.fromCallable(() -> createDefaultError(theEx, theEnv))) | ||
.cast(Object.class); | ||
} | ||
|
||
private ErrorContainer createDefaultError(Throwable ex, DataFetchingEnvironment env) { | ||
|
||
ErrorType errorType = (ex instanceof RepresentationException representationEx ? | ||
representationEx.getErrorType() : ErrorType.INTERNAL_ERROR); | ||
|
||
return new ErrorContainer(GraphqlErrorBuilder.newError(env) | ||
.errorType(errorType) | ||
.message(ex.getMessage()) | ||
.build()); | ||
} | ||
|
||
private static DataFetcherResult<List<Object>> toDataFetcherResult(List<Object> entities) { | ||
List<GraphQLError> errors = new ArrayList<>(); | ||
for (int i = 0; i < entities.size(); i++) { | ||
Object entity = entities.get(i); | ||
if (entity instanceof ErrorContainer errorContainer) { | ||
errors.addAll(errorContainer.errors()); | ||
entities.set(i, null); | ||
} | ||
} | ||
return DataFetcherResult.<List<Object>>newResult().data(entities).errors(errors).build(); | ||
} | ||
|
||
|
||
private static class EntityDataFetchingEnvironment extends DelegatingDataFetchingEnvironment { | ||
|
||
private final ExecutionStepInfo executionStepInfo; | ||
|
||
public EntityDataFetchingEnvironment(DataFetchingEnvironment env, int index) { | ||
super(env); | ||
this.executionStepInfo = ExecutionStepInfo.newExecutionStepInfo(env.getExecutionStepInfo()) | ||
.path(env.getExecutionStepInfo().getPath().segment(index)) | ||
.build(); | ||
} | ||
|
||
@Override | ||
public ExecutionStepInfo getExecutionStepInfo() { | ||
return this.executionStepInfo; | ||
} | ||
} | ||
|
||
|
||
private record ErrorContainer(List<GraphQLError> errors) { | ||
|
||
ErrorContainer(GraphQLError error) { | ||
this(Collections.singletonList(error)); | ||
} | ||
} | ||
|
||
} |
83 changes: 83 additions & 0 deletions
83
...ava/org/springframework/graphql/data/federation/EntityArgumentMethodArgumentResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* | ||
* Copyright 2002-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.data.federation; | ||
|
||
import java.util.Map; | ||
|
||
import graphql.schema.DataFetchingEnvironment; | ||
import graphql.schema.DelegatingDataFetchingEnvironment; | ||
|
||
import org.springframework.core.ResolvableType; | ||
import org.springframework.graphql.data.GraphQlArgumentBinder; | ||
import org.springframework.graphql.data.method.annotation.Argument; | ||
import org.springframework.graphql.data.method.annotation.support.ArgumentMethodArgumentResolver; | ||
import org.springframework.validation.BindException; | ||
|
||
/** | ||
* Resolver for a method parameter annotated with {@link Argument @Argument}. | ||
* On {@code @EntityMapping} methods, the raw argument value is obtained from | ||
* the "representation" input map for the entity with entries that identify | ||
* the entity uniquely. | ||
* | ||
* @author Rossen Stoyanchev | ||
* @since 1.3 | ||
*/ | ||
final class EntityArgumentMethodArgumentResolver extends ArgumentMethodArgumentResolver { | ||
|
||
|
||
EntityArgumentMethodArgumentResolver(GraphQlArgumentBinder argumentBinder) { | ||
super(argumentBinder); | ||
} | ||
|
||
|
||
@Override | ||
protected Object doBind( | ||
DataFetchingEnvironment environment, String name, ResolvableType targetType) throws BindException { | ||
|
||
if (environment instanceof EntityDataFetchingEnvironment entityEnv) { | ||
Map<String, Object> entityMap = entityEnv.getRepresentation(); | ||
Object rawValue = entityMap.get(name); | ||
boolean isOmitted = !entityMap.containsKey(name); | ||
return getArgumentBinder().bind(name, rawValue, isOmitted, targetType); | ||
} | ||
|
||
throw new IllegalStateException("Expected decorated DataFetchingEnvironment"); | ||
} | ||
|
||
/** | ||
* Wrap the environment in order to also expose the entity representation map. | ||
*/ | ||
public static DataFetchingEnvironment wrap(DataFetchingEnvironment env, Map<String, Object> representation) { | ||
return new EntityDataFetchingEnvironment(env, representation); | ||
} | ||
|
||
|
||
private static class EntityDataFetchingEnvironment extends DelegatingDataFetchingEnvironment { | ||
|
||
private final Map<String, Object> representation; | ||
|
||
EntityDataFetchingEnvironment(DataFetchingEnvironment env, Map<String, Object> representation) { | ||
super(env); | ||
this.representation = representation; | ||
} | ||
|
||
public Map<String, Object> getRepresentation() { | ||
return this.representation; | ||
} | ||
} | ||
|
||
} |
72 changes: 72 additions & 0 deletions
72
...raphql/src/main/java/org/springframework/graphql/data/federation/EntityHandlerMethod.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* | ||
* Copyright 2002-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.data.federation; | ||
|
||
import java.util.Map; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.Executor; | ||
|
||
import graphql.schema.DataFetchingEnvironment; | ||
import reactor.core.publisher.Mono; | ||
|
||
import org.springframework.graphql.data.method.HandlerMethod; | ||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite; | ||
import org.springframework.graphql.data.method.annotation.support.DataFetcherHandlerMethodSupport; | ||
import org.springframework.lang.Nullable; | ||
|
||
/** | ||
* Invokable controller method to fetch a federated entity. | ||
* | ||
* @author Rossen Stoyanchev | ||
* @since 1.3 | ||
*/ | ||
final class EntityHandlerMethod extends DataFetcherHandlerMethodSupport { | ||
|
||
public EntityHandlerMethod( | ||
HandlerMethod handlerMethod, HandlerMethodArgumentResolverComposite resolvers, | ||
@Nullable Executor executor) { | ||
|
||
super(handlerMethod, resolvers, executor); | ||
} | ||
|
||
|
||
public Mono<Object> getEntity( | ||
DataFetchingEnvironment environment, Map<String, Object> representation, int index) { | ||
|
||
Object[] args; | ||
try { | ||
environment = EntityArgumentMethodArgumentResolver.wrap(environment, representation); | ||
args = getMethodArgumentValues(environment, representation); | ||
} | ||
catch (Throwable ex) { | ||
return Mono.error(ex); | ||
} | ||
|
||
Object result = doInvoke(environment.getGraphQlContext(), args); | ||
|
||
if (result instanceof Mono<?> mono) { | ||
return mono.cast(Object.class); | ||
} | ||
else if (result instanceof CompletableFuture<?> future) { | ||
return Mono.fromFuture(future); | ||
} | ||
else { | ||
return Mono.justOrEmpty(result); | ||
} | ||
} | ||
|
||
} |
52 changes: 52 additions & 0 deletions
52
spring-graphql/src/main/java/org/springframework/graphql/data/federation/EntityMapping.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
* Copyright 2002-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.data.federation; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
import org.springframework.core.annotation.AliasFor; | ||
|
||
/** | ||
* Annotation for mapping a handler method to a federated schema type. | ||
* | ||
* @author Rossen Stoyanchev | ||
* @since 1.3 | ||
*/ | ||
@Target({ElementType.TYPE, ElementType.METHOD}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Documented | ||
public @interface EntityMapping { | ||
|
||
/** | ||
* Customize the name of the entity to map to. | ||
* <p>By default, if not specified, this is initialized from the method name, | ||
* with the first letter changed to upper case via {@link Character#toUpperCase}. | ||
*/ | ||
@AliasFor("value") | ||
String name() default ""; | ||
|
||
/** | ||
* Effectively an alias for {@link #name()}. | ||
*/ | ||
@AliasFor("name") | ||
String value() default ""; | ||
|
||
} |
Oops, something went wrong.