All Downloads are FREE. Search and download functionalities are using the official Maven repository.

graphql.execution.ExecutionStrategy Maven / Gradle / Ivy

There is a newer version: 230521-nf-execution
Show newest version
package graphql.execution;

import com.google.common.collect.ImmutableList;
import graphql.ExecutionResult;
import graphql.ExecutionResultImpl;
import graphql.GraphQLError;
import graphql.Internal;
import graphql.PublicSpi;
import graphql.SerializationError;
import graphql.TrivialDataFetcher;
import graphql.TypeMismatchError;
import graphql.UnresolvedTypeError;
import graphql.collect.ImmutableKit;
import graphql.execution.directives.QueryDirectives;
import graphql.execution.directives.QueryDirectivesImpl;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters;
import graphql.introspection.Introspection;
import graphql.language.Argument;
import graphql.language.Field;
import graphql.normalized.ExecutableNormalizedField;
import graphql.normalized.ExecutableNormalizedOperation;
import graphql.schema.CoercingSerializeException;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingFieldSelectionSet;
import graphql.schema.DataFetchingFieldSelectionSetImpl;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLCodeRegistry;
import graphql.schema.GraphQLEnumType;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import graphql.util.FpKit;
import graphql.util.LogKit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
import java.util.function.Supplier;

import static graphql.execution.Async.exceptionallyCompletedFuture;
import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo;
import static graphql.execution.FieldCollectorParameters.newParameters;
import static graphql.execution.FieldValueInfo.CompleteValueType.ENUM;
import static graphql.execution.FieldValueInfo.CompleteValueType.LIST;
import static graphql.execution.FieldValueInfo.CompleteValueType.NULL;
import static graphql.execution.FieldValueInfo.CompleteValueType.OBJECT;
import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR;
import static graphql.execution.instrumentation.SimpleInstrumentationContext.nonNullCtx;
import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment;
import static graphql.schema.GraphQLTypeUtil.isEnum;
import static graphql.schema.GraphQLTypeUtil.isList;
import static graphql.schema.GraphQLTypeUtil.isScalar;
import static java.util.concurrent.CompletableFuture.completedFuture;

/**
 * An execution strategy is give a list of fields from the graphql query to execute and find values for using a recursive strategy.
 * 
 *     query {
 *          friends {
 *              id
 *              name
 *              friends {
 *                  id
 *                  name
 *              }
 *          }
 *          enemies {
 *              id
 *              name
 *              allies {
 *                  id
 *                  name
 *              }
 *          }
 *     }
 *
 * 
*

* Given the graphql query above, an execution strategy will be called for the top level fields 'friends' and 'enemies' and it will be asked to find an object * to describe them. Because they are both complex object types, it needs to descend down that query and start fetching and completing * fields such as 'id','name' and other complex fields such as 'friends' and 'allies', by recursively calling to itself to execute these lower * field layers *

* The execution of a field has two phases, first a raw object must be fetched for a field via a {@link DataFetcher} which * is defined on the {@link GraphQLFieldDefinition}. This object must then be 'completed' into a suitable value, either as a scalar/enum type via * coercion or if it's a complex object type by recursively calling the execution strategy for the lower level fields. *

* The first phase (data fetching) is handled by the method {@link #fetchField(ExecutionContext, ExecutionStrategyParameters)} *

* The second phase (value completion) is handled by the methods {@link #completeField(ExecutionContext, ExecutionStrategyParameters, FetchedValue)} * and the other "completeXXX" methods. *

* The order of fields fetching and completion is up to the execution strategy. As the graphql specification * http://facebook.github.io/graphql/#sec-Normal-and-Serial-Execution says: *

* Normally the executor can execute the entries in a grouped field set in whatever order it chooses (often in parallel). Because * the resolution of fields other than top-level mutation fields must always be side effect-free and idempotent, the * execution order must not affect the result, and hence the server has the freedom to execute the * field entries in whatever order it deems optimal. *
*

* So in the case above you could execute the fields depth first ('friends' and its sub fields then do 'enemies' and its sub fields or it * could do breadth first ('fiends' and 'enemies' data fetch first and then all the sub fields) or in parallel via asynchronous * facilities like {@link CompletableFuture}s. *

* {@link #execute(ExecutionContext, ExecutionStrategyParameters)} is the entry point of the execution strategy. */ @PublicSpi @SuppressWarnings("FutureReturnValueIgnored") public abstract class ExecutionStrategy { private static final Logger log = LoggerFactory.getLogger(ExecutionStrategy.class); private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(ExecutionStrategy.class); protected final FieldCollector fieldCollector = new FieldCollector(); protected final ExecutionStepInfoFactory executionStepInfoFactory = new ExecutionStepInfoFactory(); private final ResolveType resolvedType = new ResolveType(); protected final DataFetcherExceptionHandler dataFetcherExceptionHandler; /** * The default execution strategy constructor uses the {@link SimpleDataFetcherExceptionHandler} * for data fetching errors. */ protected ExecutionStrategy() { dataFetcherExceptionHandler = new SimpleDataFetcherExceptionHandler(); } /** * The consumers of the execution strategy can pass in a {@link DataFetcherExceptionHandler} to better * decide what do when a data fetching error happens * * @param dataFetcherExceptionHandler the callback invoked if an exception happens during data fetching */ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHandler) { this.dataFetcherExceptionHandler = dataFetcherExceptionHandler; } /** * This is the entry point to an execution strategy. It will be passed the fields to execute and get values for. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * * @return a promise to an {@link ExecutionResult} * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ public abstract CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException; /** * Called to fetch a value for a field and resolve it further in terms of the graphql query. This will call * #fetchField followed by #completeField and the completed {@link ExecutionResult} is returned. *

* An execution strategy can iterate the fields to be executed and call this method for each one *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * * @return a promise to an {@link ExecutionResult} * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ protected CompletableFuture resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { return resolveFieldWithInfo(executionContext, parameters).thenCompose(FieldValueInfo::getFieldValue); } /** * Called to fetch a value for a field and its extra runtime info and resolve it further in terms of the graphql query. This will call * #fetchField followed by #completeField and the completed {@link graphql.execution.FieldValueInfo} is returned. *

* An execution strategy can iterate the fields to be executed and call this method for each one *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * * @return a promise to a {@link FieldValueInfo} * * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value */ protected CompletableFuture resolveFieldWithInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, parameters.getField().getSingleField()); Supplier executionStepInfo = FpKit.intraThreadMemoize(() -> createExecutionStepInfo(executionContext, parameters, fieldDef, null)); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationContext fieldCtx = nonNullCtx(instrumentation.beginField( new InstrumentationFieldParameters(executionContext, executionStepInfo), executionContext.getInstrumentationState() )); CompletableFuture fetchFieldFuture = fetchField(executionContext, parameters); CompletableFuture result = fetchFieldFuture.thenApply((fetchedValue) -> completeField(executionContext, parameters, fetchedValue)); CompletableFuture executionResultFuture = result.thenCompose(FieldValueInfo::getFieldValue); fieldCtx.onDispatched(executionResultFuture); executionResultFuture.whenComplete(fieldCtx::onCompleted); return result; } /** * Called to fetch a value for a field from the {@link DataFetcher} associated with the field * {@link GraphQLFieldDefinition}. *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * * @return a promise to a fetched object * * @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value */ protected CompletableFuture fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { MergedField field = parameters.getField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField()); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); GraphQLOutputType fieldType = fieldDef.getType(); // if the DF (like PropertyDataFetcher) does not use the arguments of execution step info then dont build any Supplier executionStepInfo = FpKit.intraThreadMemoize( () -> createExecutionStepInfo(executionContext, parameters, fieldDef, parentType)); Supplier> argumentValues = () -> executionStepInfo.get().getArguments(); Supplier normalizedFieldSupplier = getNormalizedField(executionContext, parameters, executionStepInfo); // DataFetchingFieldSelectionSet and QueryDirectives is a supplier of sorts - eg a lazy pattern DataFetchingFieldSelectionSet fieldCollector = DataFetchingFieldSelectionSetImpl.newCollector(executionContext.getGraphQLSchema(), fieldType, normalizedFieldSupplier); QueryDirectives queryDirectives = new QueryDirectivesImpl(field, executionContext.getGraphQLSchema(), executionContext.getVariables()); DataFetchingEnvironment environment = newDataFetchingEnvironment(executionContext) .source(parameters.getSource()) .localContext(parameters.getLocalContext()) .arguments(argumentValues) .fieldDefinition(fieldDef) .mergedField(parameters.getField()) .fieldType(fieldType) .executionStepInfo(executionStepInfo) .parentType(parentType) .selectionSet(fieldCollector) .queryDirectives(queryDirectives) .build(); DataFetcher dataFetcher = codeRegistry.getDataFetcher(parentType, fieldDef); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldFetchParameters instrumentationFieldFetchParams = new InstrumentationFieldFetchParameters(executionContext, environment, parameters, dataFetcher instanceof TrivialDataFetcher); InstrumentationContext fetchCtx = nonNullCtx(instrumentation.beginFieldFetch(instrumentationFieldFetchParams, executionContext.getInstrumentationState()) ); CompletableFuture fetchedValue; dataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, instrumentationFieldFetchParams, executionContext.getInstrumentationState()); ExecutionId executionId = executionContext.getExecutionId(); try { Object fetchedValueRaw = dataFetcher.get(environment); fetchedValue = Async.toCompletableFuture(fetchedValueRaw); } catch (Exception e) { if (logNotSafe.isDebugEnabled()) { logNotSafe.debug(String.format("'%s', field '%s' fetch threw exception", executionId, executionStepInfo.get().getPath()), e); } fetchedValue = new CompletableFuture<>(); fetchedValue.completeExceptionally(e); } fetchCtx.onDispatched(fetchedValue); return fetchedValue .handle((result, exception) -> { fetchCtx.onCompleted(result, exception); if (exception != null) { return handleFetchingException(executionContext, environment, exception); } else { return CompletableFuture.completedFuture(result); } }) .thenCompose(Function.identity()) .thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result)); } protected Supplier getNormalizedField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Supplier executionStepInfo) { Supplier normalizedQuery = executionContext.getNormalizedQueryTree(); return () -> normalizedQuery.get().getNormalizedField(parameters.getField(), executionStepInfo.get().getObjectType(), executionStepInfo.get().getPath()); } protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Object result) { if (result instanceof DataFetcherResult) { DataFetcherResult dataFetcherResult = (DataFetcherResult) result; 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(); } return FetchedValue.newFetchedValue() .fetchedValue(executionContext.getValueUnboxer().unbox(dataFetcherResult.getData())) .rawFetchedValue(dataFetcherResult.getData()) .errors(dataFetcherResult.getErrors()) .localContext(localContext) .build(); } else { return FetchedValue.newFetchedValue() .fetchedValue(executionContext.getValueUnboxer().unbox(result)) .rawFetchedValue(result) .localContext(parameters.getLocalContext()) .build(); } } protected CompletableFuture handleFetchingException(ExecutionContext executionContext, DataFetchingEnvironment environment, Throwable e) { DataFetcherExceptionHandlerParameters handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() .dataFetchingEnvironment(environment) .exception(e) .build(); try { return asyncHandleException(dataFetcherExceptionHandler, handlerParameters, executionContext); } catch (Exception handlerException) { handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters() .dataFetchingEnvironment(environment) .exception(handlerException) .build(); return asyncHandleException(new SimpleDataFetcherExceptionHandler(), handlerParameters, executionContext); } } private CompletableFuture asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters, ExecutionContext executionContext) { //noinspection unchecked return handler.handleException(handlerParameters) .thenApply(handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build() ); } /** * Called to complete a field based on the type of the field. *

* If the field is a scalar type, then it will be coerced and returned. However if the field type is an complex object type, then * the execution strategy will be called recursively again to execute the fields of that type before returning. *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param fetchedValue the fetched raw value * * @return a {@link FieldValueInfo} * * @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValue()} future if a non null field resolves to a null value */ protected FieldValueInfo completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) { Field field = parameters.getField().getSingleField(); GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field); ExecutionStepInfo executionStepInfo = createExecutionStepInfo(executionContext, parameters, fieldDef, parentType); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, fetchedValue); InstrumentationContext ctxCompleteField = nonNullCtx(instrumentation.beginFieldComplete( instrumentationParams, executionContext.getInstrumentationState() )); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo); ExecutionStrategyParameters newParameters = parameters.transform(builder -> builder.executionStepInfo(executionStepInfo) .source(fetchedValue.getFetchedValue()) .localContext(fetchedValue.getLocalContext()) .nonNullFieldValidator(nonNullableFieldValidator) ); if (log.isDebugEnabled()) { log.debug("'{}' completing field '{}'...", executionContext.getExecutionId(), executionStepInfo.getPath()); } FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters); CompletableFuture executionResultFuture = fieldValueInfo.getFieldValue(); ctxCompleteField.onDispatched(executionResultFuture); executionResultFuture.whenComplete(ctxCompleteField::onCompleted); return fieldValueInfo; } /** * Called to complete a value for a field based on the type of the field. *

* If the field is a scalar type, then it will be coerced and returned. However if the field type is an complex object type, then * the execution strategy will be called recursively again to execute the fields of that type before returning. *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * * @return a {@link FieldValueInfo} * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected FieldValueInfo completeValue(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); Object result = executionContext.getValueUnboxer().unbox(parameters.getSource()); GraphQLType fieldType = executionStepInfo.getUnwrappedNonNullType(); CompletableFuture fieldValue; if (result == null) { fieldValue = completeValueForNull(executionContext, parameters); return FieldValueInfo.newFieldValueInfo(NULL).fieldValue(fieldValue).build(); } else if (isList(fieldType)) { return completeValueForList(executionContext, parameters, result); } else if (isScalar(fieldType)) { fieldValue = completeValueForScalar(executionContext, parameters, (GraphQLScalarType) fieldType, result); return FieldValueInfo.newFieldValueInfo(SCALAR).fieldValue(fieldValue).build(); } else if (isEnum(fieldType)) { fieldValue = completeValueForEnum(executionContext, parameters, (GraphQLEnumType) fieldType, result); return FieldValueInfo.newFieldValueInfo(ENUM).fieldValue(fieldValue).build(); } // when we are here, we have a complex type: Interface, Union or Object // and we must go deeper // GraphQLObjectType resolvedObjectType; try { resolvedObjectType = resolveType(executionContext, parameters, fieldType); fieldValue = completeValueForObject(executionContext, parameters, resolvedObjectType, result); } catch (UnresolvedTypeException ex) { // consider the result to be null and add the error on the context handleUnresolvedTypeProblem(executionContext, parameters, ex); // and validate the field is nullable, if non-nullable throw exception parameters.getNonNullFieldValidator().validate(parameters.getPath(), null); // complete the field as null fieldValue = completedFuture(new ExecutionResultImpl(null, executionContext.getErrors())); } return FieldValueInfo.newFieldValueInfo(OBJECT).fieldValue(fieldValue).build(); } private void handleUnresolvedTypeProblem(ExecutionContext context, ExecutionStrategyParameters parameters, UnresolvedTypeException e) { UnresolvedTypeError error = new UnresolvedTypeError(parameters.getPath(), parameters.getExecutionStepInfo(), e); logNotSafe.warn(error.getMessage(), e); context.addError(error); } protected CompletableFuture completeValueForNull(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { return Async.tryCatch(() -> { Object nullValue = parameters.getNonNullFieldValidator().validate(parameters.getPath(), null); return completedFuture(new ExecutionResultImpl(nullValue, executionContext.getErrors())); }); } /** * Called to complete a list of value for a field based on a list type. This iterates the values and calls * {@link #completeValue(ExecutionContext, ExecutionStrategyParameters)} for each value. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param result the result to complete, raw result * * @return a {@link FieldValueInfo} */ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Object result) { Iterable resultIterable = toIterable(executionContext, parameters, result); try { resultIterable = parameters.getNonNullFieldValidator().validate(parameters.getPath(), resultIterable); } catch (NonNullableFieldWasNullException e) { return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(exceptionallyCompletedFuture(e)).build(); } if (resultIterable == null) { return FieldValueInfo.newFieldValueInfo(LIST).fieldValue(completedFuture(new ExecutionResultImpl(null, executionContext.getErrors()))).build(); } return completeValueForList(executionContext, parameters, resultIterable); } /** * Called to complete a list of value for a field based on a list type. This iterates the values and calls * {@link #completeValue(ExecutionContext, ExecutionStrategyParameters)} for each value. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param iterableValues the values to complete, can't be null * * @return a {@link FieldValueInfo} */ protected FieldValueInfo completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Iterable iterableValues) { OptionalInt size = FpKit.toSize(iterableValues); ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, iterableValues); Instrumentation instrumentation = executionContext.getInstrumentation(); InstrumentationContext completeListCtx = nonNullCtx(instrumentation.beginFieldListComplete( instrumentationParams, executionContext.getInstrumentationState() )); List fieldValueInfos = new ArrayList<>(size.orElse(1)); int index = 0; for (Object item : iterableValues) { ResultPath indexedPath = parameters.getPath().segment(index); ExecutionStepInfo stepInfoForListElement = executionStepInfoFactory.newExecutionStepInfoForListElement(executionStepInfo, index); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, stepInfoForListElement); int finalIndex = index; FetchedValue value = unboxPossibleDataFetcherResult(executionContext, parameters, item); ExecutionStrategyParameters newParameters = parameters.transform(builder -> builder.executionStepInfo(stepInfoForListElement) .nonNullFieldValidator(nonNullableFieldValidator) .listSize(size.orElse(-1)) // -1 signals that we don't know the size .localContext(value.getLocalContext()) .currentListIndex(finalIndex) .path(indexedPath) .source(value.getFetchedValue()) ); fieldValueInfos.add(completeValue(executionContext, newParameters)); index++; } CompletableFuture> resultsFuture = Async.each(fieldValueInfos, (item, i) -> item.getFieldValue()); CompletableFuture overallResult = new CompletableFuture<>(); completeListCtx.onDispatched(overallResult); resultsFuture.whenComplete((results, exception) -> { if (exception != null) { ExecutionResult executionResult = handleNonNullException(executionContext, overallResult, exception); completeListCtx.onCompleted(executionResult, exception); return; } List completedResults = new ArrayList<>(results.size()); for (ExecutionResult completedValue : results) { completedResults.add(completedValue.getData()); } ExecutionResultImpl executionResult = new ExecutionResultImpl(completedResults, executionContext.getErrors()); overallResult.complete(executionResult); }); overallResult.whenComplete(completeListCtx::onCompleted); return FieldValueInfo.newFieldValueInfo(LIST) .fieldValue(overallResult) .fieldValueInfos(fieldValueInfos) .build(); } /** * Called to turn an object into a scalar value according to the {@link GraphQLScalarType} by asking that scalar type to coerce the object * into a valid value * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param scalarType the type of the scalar * @param result the result to be coerced * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { Object serialized; try { serialized = scalarType.getCoercing().serialize(result); } catch (CoercingSerializeException e) { serialized = handleCoercionProblem(executionContext, parameters, e); } // TODO: fix that: this should not be handled here //6.6.1 http://facebook.github.io/graphql/#sec-Field-entries if (serialized instanceof Double && ((Double) serialized).isNaN()) { serialized = null; } try { serialized = parameters.getNonNullFieldValidator().validate(parameters.getPath(), serialized); } catch (NonNullableFieldWasNullException e) { return exceptionallyCompletedFuture(e); } return completedFuture(new ExecutionResultImpl(serialized, executionContext.getErrors())); } /** * Called to turn an object into a enum value according to the {@link GraphQLEnumType} by asking that enum type to coerce the object into a valid value * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param enumType the type of the enum * @param result the result to be coerced * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { Object serialized; try { serialized = enumType.serialize(result); } catch (CoercingSerializeException e) { serialized = handleCoercionProblem(executionContext, parameters, e); } try { serialized = parameters.getNonNullFieldValidator().validate(parameters.getPath(), serialized); } catch (NonNullableFieldWasNullException e) { return exceptionallyCompletedFuture(e); } return completedFuture(new ExecutionResultImpl(serialized, executionContext.getErrors())); } /** * Called to turn an java object value into an graphql object value * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param resolvedObjectType the resolved object type * @param result the result to be coerced * * @return a promise to an {@link ExecutionResult} */ protected CompletableFuture completeValueForObject(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLObjectType resolvedObjectType, Object result) { ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo(); FieldCollectorParameters collectorParameters = newParameters() .schema(executionContext.getGraphQLSchema()) .objectType(resolvedObjectType) .fragments(executionContext.getFragmentsByName()) .variables(executionContext.getVariables()) .build(); MergedSelectionSet subFields = fieldCollector.collectFields(collectorParameters, parameters.getField()); ExecutionStepInfo newExecutionStepInfo = executionStepInfo.changeTypeWithPreservedNonNull(resolvedObjectType); NonNullableFieldValidator nonNullableFieldValidator = new NonNullableFieldValidator(executionContext, newExecutionStepInfo); ExecutionStrategyParameters newParameters = parameters.transform(builder -> builder.executionStepInfo(newExecutionStepInfo) .fields(subFields) .nonNullFieldValidator(nonNullableFieldValidator) .source(result) ); // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. return executionContext.getQueryStrategy().execute(executionContext, newParameters); } @SuppressWarnings("SameReturnValue") private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategyParameters parameters, CoercingSerializeException e) { SerializationError error = new SerializationError(parameters.getPath(), e); logNotSafe.warn(error.getMessage(), e); context.addError(error); return null; } /** * Converts an object that is known to should be an Iterable into one * * @param result the result object * * @return an Iterable from that object * * @throws java.lang.ClassCastException if it's not an Iterable */ protected Iterable toIterable(Object result) { return FpKit.toIterable(result); } protected GraphQLObjectType resolveType(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLType fieldType) { return resolvedType.resolveType(executionContext, parameters.getField(), parameters.getSource(), parameters.getExecutionStepInfo(), fieldType, parameters.getLocalContext()); } protected Iterable toIterable(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) { if (FpKit.isIterable(result)) { return FpKit.toIterable(result); } handleTypeMismatchProblem(context, parameters, result); return null; } private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) { TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getExecutionStepInfo().getUnwrappedNonNullType()); logNotSafe.warn("{} got {}", error.getMessage(), result.getClass()); context.addError(error); } /** * Called to discover the field definition give the current parameters and the AST {@link Field} * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param field the field to find the definition of * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Field field) { GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType(); return getFieldDef(executionContext.getGraphQLSchema(), parentType, field); } /** * Called to discover the field definition give the current parameters and the AST {@link Field} * * @param schema the schema in play * @param parentType the parent type of the field * @param field the field to find the definition of * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObjectType parentType, Field field) { return Introspection.getFieldDef(schema, parentType, field.getName()); } /** * See (http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability), *

* If a non nullable child field type actually resolves to a null value and the parent type is nullable * then the parent must in fact become null * so we use exceptions to indicate this special case. However if the parent is in fact a non nullable type * itself then we need to bubble that upwards again until we get to the root in which case the result * is meant to be null. * * @param e this indicates that a null value was returned for a non null field, which needs to cause the parent field * to become null OR continue on as an exception * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected void assertNonNullFieldPrecondition(NonNullableFieldWasNullException e) throws NonNullableFieldWasNullException { ExecutionStepInfo executionStepInfo = e.getExecutionStepInfo(); if (executionStepInfo.hasParent() && executionStepInfo.getParent().isNonNullType()) { throw new NonNullableFieldWasNullException(e); } } protected void assertNonNullFieldPrecondition(NonNullableFieldWasNullException e, CompletableFuture completableFuture) throws NonNullableFieldWasNullException { ExecutionStepInfo executionStepInfo = e.getExecutionStepInfo(); if (executionStepInfo.hasParent() && executionStepInfo.getParent().isNonNullType()) { completableFuture.completeExceptionally(new NonNullableFieldWasNullException(e)); } } protected ExecutionResult handleNonNullException(ExecutionContext executionContext, CompletableFuture result, Throwable e) { ExecutionResult executionResult = null; List errors = ImmutableList.copyOf(executionContext.getErrors()); Throwable underlyingException = e; if (e instanceof CompletionException) { underlyingException = e.getCause(); } if (underlyingException instanceof NonNullableFieldWasNullException) { assertNonNullFieldPrecondition((NonNullableFieldWasNullException) underlyingException, result); if (!result.isDone()) { executionResult = new ExecutionResultImpl(null, errors); result.complete(executionResult); } } else if (underlyingException instanceof AbortExecutionException) { AbortExecutionException abortException = (AbortExecutionException) underlyingException; executionResult = abortException.toExecutionResult(); result.complete(executionResult); } else { result.completeExceptionally(e); } return executionResult; } /** * Builds the type info hierarchy for the current field * * @param executionContext the execution context in play * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldDefinition the field definition to build type info for * @param fieldContainer the field container * * @return a new type info */ protected ExecutionStepInfo createExecutionStepInfo(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLFieldDefinition fieldDefinition, GraphQLObjectType fieldContainer) { MergedField field = parameters.getField(); ExecutionStepInfo parentStepInfo = parameters.getExecutionStepInfo(); GraphQLOutputType fieldType = fieldDefinition.getType(); List fieldArgDefs = fieldDefinition.getArguments(); Supplier> argumentValues = ImmutableKit::emptyMap; // // no need to create args at all if there are none on the field def // if (!fieldArgDefs.isEmpty()) { List fieldArgs = field.getArguments(); GraphQLCodeRegistry codeRegistry = executionContext.getGraphQLSchema().getCodeRegistry(); argumentValues = FpKit.intraThreadMemoize(() -> ValuesResolver.getArgumentValues(codeRegistry, fieldArgDefs, fieldArgs, executionContext.getCoercedVariables())); } return newExecutionStepInfo() .type(fieldType) .fieldDefinition(fieldDefinition) .fieldContainer(fieldContainer) .field(field) .path(parameters.getPath()) .parentInfo(parentStepInfo) .arguments(argumentValues) .build(); } @Internal public static String mkNameForPath(Field currentField) { return mkNameForPath(Collections.singletonList(currentField)); } @Internal public static String mkNameForPath(MergedField mergedField) { return mkNameForPath(mergedField.getFields()); } @Internal public static String mkNameForPath(List currentField) { Field field = currentField.get(0); return field.getResultKey(); } }