Replies: 4 comments 6 replies
-
expected response:
|
Beta Was this translation helpful? Give feedback.
-
Agreed that this will be somewhat difficult to workaround. My plan is as follows:
Hoping others have suggestions on how they tackled this concern. |
Beta Was this translation helpful? Give feedback.
-
So how you usually handle error in data (with your own custom data structure)? Any more centralize way to do this (ala Controller advice)? On Spring graphql you also have an option to use WebGraphQlInterceptor, which allow you to modify the response (add error in data, etc.) |
Beta Was this translation helpful? Give feedback.
-
We ended up switching to Spring GraphQL recently, but still had to come up with a solution for this. So we created a custom annotation to handle it and then overrode the execution strategy to pull the default if the fetched value was null. Annotation definition import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation provides the ability to define a default value to use for a fetched result when no result is returned by a data fetcher. This may be
* beneficial when resolving a non-nullable field and an exception occurs. It allows GraphQL errors to be returned and a default value. The
* {@link GraphQLDefaultAnnotationInterceptor} intercepts and adds the default value onto the {@link graphql.GraphQLContext} which the
* {@link DefaultHandlingExecutionStrategy} uses to apply the default value.
* <p>
* <b>This annotation only applies on functions that have a {@link graphql.schema.DataFetchingEnvironment} or {@link graphql.GraphQLContext} as an argument.</b>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GraphQLDefault {
enum DefaultType {
LIST,
OPTIONAL
}
DefaultType value();
} Aspect import graphql.GraphQLContext;
import graphql.execution.MergedField;
import graphql.schema.DataFetchingEnvironment;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @see GraphQLDefault
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class GraphQLDefaultAnnotationInterceptor {
/**
* Matching DataFetchingEnvironment argument in any order
**/
@Before("@annotation(graphQLDefault) && args(dfe,..)")
public void interceptAsDfeFirstParam(GraphQLDefault graphQLDefault, DataFetchingEnvironment dfe) {
setDefaultToContext(graphQLDefault, dfe);
}
@Before("@annotation(graphQLDefault) && args(..,dfe)")
public void interceptAsDfeLastParam(GraphQLDefault graphQLDefault, DataFetchingEnvironment dfe) {
setDefaultToContext(graphQLDefault, dfe);
}
@Before("@annotation(graphQLDefault) && args(*,dfe,..)")
public void interceptAsDfeMiddleParam(GraphQLDefault graphQLDefault, DataFetchingEnvironment dfe) {
setDefaultToContext(graphQLDefault, dfe);
}
private void setDefaultToContext(GraphQLDefault graphQLDefault, DataFetchingEnvironment dfe) {
GraphQLContext graphQlContext = dfe.getGraphQlContext();
if (!graphQlContext.hasKey(DEFAULT_ANNOTATION_KEY)) {
graphQlContext.put(DEFAULT_ANNOTATION_KEY, new HashMap<>());
}
MergedField parentField = dfe.getExecutionStepInfo().getParent() != null ? dfe.getExecutionStepInfo().getParent().getField() : null;
String fieldIdentifier = GraphQLDefaultAnnotationHelper.getFieldIdentifier(dfe.getExecutionStepInfo().getField(), parentField);
GraphQLDefaultAnnotationHelper.putDefaultValue(dfe.getGraphQlContext(), fieldIdentifier, getDefaultValue(graphQLDefault.value()));
}
private Object getDefaultValue(DefaultType defaultType) {
if (defaultType == DefaultType.LIST) {
return List.of();
} else if (defaultType == DefaultType.OPTIONAL) {
return Optional.empty();
} else {
throw new IllegalArgumentException("Unexpected defaultType " + defaultType);
}
}
} Helper class import graphql.GraphQLContext;
import graphql.execution.MergedField;
import java.util.Map;
/**
* @see GraphQLDefault
* @see GraphQLDefaultAnnotationInterceptor
*/
public final class GraphQLDefaultAnnotationHelper {
public static final String DEFAULT_ANNOTATION_KEY = "graphqlDefaultValue";
private GraphQLDefaultAnnotationHelper() {
}
public static boolean hasDefaultValue(GraphQLContext graphQlContext, String fieldIdentifier) {
if (graphQlContext.get(DEFAULT_ANNOTATION_KEY) != null) {
Map<String, Object> defaultMap = graphQlContext.get(DEFAULT_ANNOTATION_KEY);
return defaultMap.containsKey(fieldIdentifier);
}
return false;
}
public static Object getDefaultValue(GraphQLContext graphQlContext, String fieldIdentifier) {
Map<String, Object> defaultMap = graphQlContext.get(DEFAULT_ANNOTATION_KEY);
return defaultMap.get(fieldIdentifier);
}
public static void putDefaultValue(GraphQLContext graphQlContext, String fieldIdentifier, Object defaultValue) {
Map<String, Object> defaultMap = graphQlContext.get(DEFAULT_ANNOTATION_KEY);
defaultMap.put(fieldIdentifier, defaultValue);
}
public static String getFieldIdentifier(MergedField curField, MergedField parentField) {
String parentName = parentField != null ? parentField.getName() : "";
String fieldName = curField != null ? curField.getName() : "";
return parentName + "." + fieldName;
}
} Custom execution strategy which uses the default value as specified by the annotation if the fetched value is null. import graphql.GraphQLContext;
import graphql.execution.AsyncExecutionStrategy;
import graphql.execution.DataFetcherExceptionHandler;
import graphql.execution.DataFetcherResult;
import graphql.execution.ExecutionContext;
import graphql.execution.ExecutionStrategyParameters;
import graphql.execution.FetchedValue;
import graphql.execution.MergedField;
public class DefaultHandlingExecutionStrategy extends AsyncExecutionStrategy {
public DefaultHandlingExecutionStrategy(DataFetcherExceptionHandler exceptionHandler) {
super(exceptionHandler);
}
@Override
protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Object result) {
if (result instanceof DataFetcherResult<?> dataFetcherResult) {
executionContext.addErrors(dataFetcherResult.getErrors());
Object localContext = dataFetcherResult.getLocalContext();
if (localContext == null) {
// if the field returns nothing then they get the context of their parent field
localContext = parameters.getLocalContext();
}
Object value = executionContext.getValueUnboxer().unbox(dataFetcherResult.getData());
// Default value handling
GraphQLContext graphQlContext = executionContext.getGraphQLContext();
MergedField parentField = parameters.getParent() != null ? parameters.getParent().getField() : null;
String fieldIdentifier = GraphQLDefaultAnnotationHelper.getFieldIdentifier(parameters.getField(), parentField);
if (value == null && GraphQLDefaultAnnotationHelper.hasDefaultValue(graphQlContext, fieldIdentifier)) {
value = GraphQLDefaultAnnotationHelper.getDefaultValue(graphQlContext, fieldIdentifier);
}
return FetchedValue.newFetchedValue()
.fetchedValue(value)
.rawFetchedValue(dataFetcherResult.getData())
.errors(dataFetcherResult.getErrors())
.localContext(localContext)
.build();
} else {
return FetchedValue.newFetchedValue()
.fetchedValue(executionContext.getValueUnboxer().unbox(result))
.rawFetchedValue(result)
.localContext(parameters.getLocalContext())
.build();
}
}
} This will allow you to annotate any datafetcher function with either |
Beta Was this translation helpful? Give feedback.
-
The doc says: "Responses can contain both data and errors, for example when some fields where resolved successfully, but other fields had errors. A field with an error is set to null, and an error is added to the errors block."
but the data is null in my case. Am I missing something here?
graphqls:
code:
query:
response:
Beta Was this translation helpful? Give feedback.
All reactions