Skip to content
Merged
78 changes: 47 additions & 31 deletions src/main/java/graphql/execution/ExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
* <p>
* The first phase (data fetching) is handled by the method {@link #fetchField(ExecutionContext, ExecutionStrategyParameters)}
* <p>
* The second phase (value completion) is handled by the methods {@link #completeField(ExecutionContext, ExecutionStrategyParameters, FetchedValue)}
* The second phase (value completion) is handled by the methods {@link #completeField(ExecutionContext, ExecutionStrategyParameters, Object)}
* and the other "completeXXX" methods.
* <p>
* The order of fields fetching and completion is up to the execution strategy. As the graphql specification
Expand Down Expand Up @@ -358,7 +358,7 @@ protected Object resolveFieldWithInfo(ExecutionContext executionContext, Executi

Object fetchedValueObj = fetchField(executionContext, parameters);
if (fetchedValueObj instanceof CompletableFuture) {
CompletableFuture<FetchedValue> fetchFieldFuture = (CompletableFuture<FetchedValue>) fetchedValueObj;
CompletableFuture<Object> fetchFieldFuture = (CompletableFuture<Object>) fetchedValueObj;
CompletableFuture<FieldValueInfo> result = fetchFieldFuture.thenApply((fetchedValue) ->
completeField(fieldDef, executionContext, parameters, fetchedValue));

Expand All @@ -367,10 +367,9 @@ protected Object resolveFieldWithInfo(ExecutionContext executionContext, Executi
return result;
} else {
try {
FetchedValue fetchedValue = (FetchedValue) fetchedValueObj;
FieldValueInfo fieldValueInfo = completeField(fieldDef, executionContext, parameters, fetchedValue);
FieldValueInfo fieldValueInfo = completeField(fieldDef, executionContext, parameters, fetchedValueObj);
fieldCtx.onDispatched();
fieldCtx.onCompleted(fetchedValue.getFetchedValue(), null);
fieldCtx.onCompleted(FetchedValue.getFetchedValue(fetchedValueObj), null);
return fieldValueInfo;
} catch (Exception e) {
return Async.exceptionallyCompletedFuture(e);
Expand All @@ -383,29 +382,29 @@ protected Object resolveFieldWithInfo(ExecutionContext executionContext, Executi
* {@link GraphQLFieldDefinition}.
* <p>
* 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.
* 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 FetchedValue} object or the {@link FetchedValue} itself
* @return a promise to a value object or the value itself. The value maybe a raw object OR a {@link FetchedValue}
*
* @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value
* @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value
*/
@DuckTyped(shape = "CompletableFuture<FetchedValue> | FetchedValue")
@DuckTyped(shape = "CompletableFuture<FetchedValue|Object> | <FetchedValue|Object>")
protected Object fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
MergedField field = parameters.getField();
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();
GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField());
return fetchField(fieldDef, executionContext, parameters);
}

@DuckTyped(shape = "CompletableFuture<FetchedValue> | FetchedValue")
@DuckTyped(shape = "CompletableFuture<FetchedValue|Object> | <FetchedValue|Object>")
private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
executionContext.throwIfCancelled();

if (incrementAndCheckMaxNodesExceeded(executionContext)) {
return new FetchedValue(null, Collections.emptyList(), null);
return null;
}

MergedField field = parameters.getField();
Expand Down Expand Up @@ -486,9 +485,8 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec
}
});
CompletableFuture<Object> rawResultCF = engineRunningState.compose(handleCF, Function.identity());
CompletableFuture<FetchedValue> fetchedValueCF = rawResultCF
return rawResultCF
.thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result));
return fetchedValueCF;
} else {
fetchCtx.onCompleted(fetchedObject, null);
return unboxPossibleDataFetcherResult(executionContext, parameters, fetchedObject);
Expand Down Expand Up @@ -520,9 +518,21 @@ protected Supplier<ExecutableNormalizedField> getNormalizedField(ExecutionContex
return () -> normalizedQuery.get().getNormalizedField(parameters.getField(), executionStepInfo.get().getObjectType(), executionStepInfo.get().getPath());
}

protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext executionContext,
ExecutionStrategyParameters parameters,
Object result) {
/**
* If the data fetching returned a {@link DataFetcherResult} then it can contain errors and new local context
* and hence it gets turned into a {@link FetchedValue} but otherwise this method returns the unboxed
* value without the wrapper. This means its more efficient overall by default.
*
* @param executionContext the execution context in play
* @param parameters the parameters in play
* @param result the fetched raw object
*
* @return an unboxed value which can be a FetchedValue or an Object
*/
@DuckTyped(shape = "FetchedValue | Object")
protected Object unboxPossibleDataFetcherResult(ExecutionContext executionContext,
ExecutionStrategyParameters parameters,
Object result) {
if (result instanceof DataFetcherResult) {
DataFetcherResult<?> dataFetcherResult = (DataFetcherResult<?>) result;

Expand All @@ -538,8 +548,7 @@ protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext execution
Object unBoxedValue = executionContext.getValueUnboxer().unbox(dataFetcherResult.getData());
return new FetchedValue(unBoxedValue, dataFetcherResult.getErrors(), localContext);
} else {
Object unBoxedValue = executionContext.getValueUnboxer().unbox(result);
return new FetchedValue(unBoxedValue, ImmutableList.of(), parameters.getLocalContext());
return executionContext.getValueUnboxer().unbox(result);
}
}

Expand Down Expand Up @@ -577,29 +586,32 @@ protected <T> CompletableFuture<T> handleFetchingException(
private <T> CompletableFuture<T> asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) {
//noinspection unchecked
return handler.handleException(handlerParameters).thenApply(
handlerResult -> (T) DataFetcherResult.<FetchedValue>newResult().errors(handlerResult.getErrors()).build()
handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build()
);
}

/**
* Called to complete a field based on the type of the field.
* <p>
* 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
* 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.
* <p>
* 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.
* 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
* @param fetchedValue the fetched raw value or perhaps a {@link FetchedValue} wrapper of that value
*
* @return a {@link FieldValueInfo}
*
* @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValueFuture()} future
* if a nonnull field resolves to a null value
*/
protected FieldValueInfo completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) {
protected FieldValueInfo completeField(ExecutionContext executionContext,
ExecutionStrategyParameters parameters,
@DuckTyped(shape = "Object | FetchedValue")
Object fetchedValue) {
executionContext.throwIfCancelled();

Field field = parameters.getField().getSingleField();
Expand All @@ -608,7 +620,7 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut
return completeField(fieldDef, executionContext, parameters, fetchedValue);
}

private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) {
private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters, Object fetchedValue) {
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();
ExecutionStepInfo executionStepInfo = createExecutionStepInfo(executionContext, parameters, fieldDef, parentType);

Expand All @@ -618,9 +630,11 @@ private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionC
instrumentationParams, executionContext.getInstrumentationState()
));

ExecutionStrategyParameters newParameters = parameters.transform(executionStepInfo,
fetchedValue.getLocalContext(),
fetchedValue.getFetchedValue());
ExecutionStrategyParameters newParameters = parameters.transform(
executionStepInfo,
FetchedValue.getLocalContext(fetchedValue, parameters.getLocalContext()),
FetchedValue.getFetchedValue(fetchedValue)
);

FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters);
ctxCompleteField.onDispatched();
Expand Down Expand Up @@ -777,12 +791,14 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext,

ExecutionStepInfo stepInfoForListElement = executionStepInfoFactory.newExecutionStepInfoForListElement(executionStepInfo, indexedPath);

FetchedValue value = unboxPossibleDataFetcherResult(executionContext, parameters, item);
Object fetchedValue = unboxPossibleDataFetcherResult(executionContext, parameters, item);

ExecutionStrategyParameters newParameters = parameters.transform(stepInfoForListElement,
ExecutionStrategyParameters newParameters = parameters.transform(
stepInfoForListElement,
indexedPath,
value.getLocalContext(),
value.getFetchedValue());
FetchedValue.getLocalContext(fetchedValue, parameters.getLocalContext()),
FetchedValue.getFetchedValue(fetchedValue)
);

fieldValueInfos.add(completeValue(executionContext, newParameters));
index++;
Expand Down
35 changes: 34 additions & 1 deletion src/main/java/graphql/execution/FetchedValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import java.util.List;

/**
* Note: This is returned by {@link InstrumentationFieldCompleteParameters#getFetchedValue()}
* Note: This MAY be returned by {@link InstrumentationFieldCompleteParameters#getFetchedObject()}
* and therefore part of the public despite never used in a method signature.
*/
@PublicApi
Expand All @@ -17,6 +17,39 @@ public class FetchedValue {
private final Object localContext;
private final ImmutableList<GraphQLError> errors;

/**
* This allows you to get to the underlying fetched value depending on whether the source
* value is a {@link FetchedValue} or not
*
* @param sourceValue the source value in play
*
* @return the {@link FetchedValue#getFetchedValue()} if its wrapped otherwise the source value itself
*/
public static Object getFetchedValue(Object sourceValue) {
if (sourceValue instanceof FetchedValue) {
return ((FetchedValue) sourceValue).fetchedValue;
} else {
return sourceValue;
}
}

/**
* This allows you to get to the local context depending on whether the source
* value is a {@link FetchedValue} or not
*
* @param sourceValue the source value in play
* @param defaultLocalContext the default local context to use
*
* @return the {@link FetchedValue#getFetchedValue()} if its wrapped otherwise the default local context
*/
public static Object getLocalContext(Object sourceValue, Object defaultLocalContext) {
if (sourceValue instanceof FetchedValue) {
return ((FetchedValue) sourceValue).localContext;
} else {
return defaultLocalContext;
}
}

public FetchedValue(Object fetchedValue, List<GraphQLError> errors, Object localContext) {
this.fetchedValue = fetchedValue;
this.errors = ImmutableList.copyOf(errors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ private boolean keepOrdered(GraphQLContext graphQLContext) {
private CompletableFuture<Publisher<Object>> createSourceEventStream(ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
ExecutionStrategyParameters newParameters = firstFieldOfSubscriptionSelection(executionContext, parameters, false);

CompletableFuture<FetchedValue> fieldFetched = Async.toCompletableFuture(fetchField(executionContext, newParameters));
CompletableFuture<Object> fieldFetched = Async.toCompletableFuture(fetchField(executionContext, newParameters));
return fieldFetched.thenApply(fetchedValue -> {
Object publisher = fetchedValue.getFetchedValue();
Object publisher = FetchedValue.getFetchedValue(fetchedValue);
return mkReactivePublisher(publisher);
});
}
Expand Down Expand Up @@ -168,7 +168,7 @@ private CompletableFuture<ExecutionResult> executeSubscriptionEvent(ExecutionCon
i13nFieldParameters, executionContext.getInstrumentationState()
));

FetchedValue fetchedValue = unboxPossibleDataFetcherResult(newExecutionContext, newParameters, eventPayload);
Object fetchedValue = unboxPossibleDataFetcherResult(newExecutionContext, newParameters, eventPayload);
FieldValueInfo fieldValueInfo = completeField(newExecutionContext, newParameters, fetchedValue);
executionContext.getDataLoaderDispatcherStrategy().newSubscriptionExecution(fieldValueInfo, newParameters.getDeferredCallContext());
CompletableFuture<ExecutionResult> overallResult = fieldValueInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ public ExecutionStepInfo getExecutionStepInfo() {
return executionStepInfo.get();
}

public Object getFetchedValue() {
/**
* This returns the object that was fetched, ready to be completed as a value. This can sometimes be a {@link graphql.execution.FetchedValue} object
* but most often it's a simple POJO.
*
* @return the object was fetched, ready to be completed as a value.
*/
public Object getFetchedObject() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the hard breaking change - the new method means they HAVE to handle it should they be using the old method

return fetchedValue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ public BreadthFirstExecutionTestStrategy() {
public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
MergedSelectionSet fields = parameters.getFields();

Map<String, FetchedValue> fetchedValues = new LinkedHashMap<>();
Map<String, Object> fetchedValues = new LinkedHashMap<>();

// first fetch every value
for (String fieldName : fields.keySet()) {
FetchedValue fetchedValue = fetchField(executionContext, parameters, fields, fieldName);
Object fetchedValue = fetchField(executionContext, parameters, fields, fieldName);
fetchedValues.put(fieldName, fetchedValue);
}

// then for every fetched value, complete it
Map<String, Object> results = new LinkedHashMap<>();
for (String fieldName : fetchedValues.keySet()) {
MergedField currentField = fields.getSubField(fieldName);
FetchedValue fetchedValue = fetchedValues.get(fieldName);
Object fetchedValue = fetchedValues.get(fieldName);

ResultPath fieldPath = parameters.getPath().segment(fieldName);
ExecutionStrategyParameters newParameters = parameters
Expand All @@ -51,17 +51,17 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
return CompletableFuture.completedFuture(new ExecutionResultImpl(results, executionContext.getErrors()));
}

private FetchedValue fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedSelectionSet fields, String fieldName) {
private Object fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedSelectionSet fields, String fieldName) {
MergedField currentField = fields.getSubField(fieldName);

ResultPath fieldPath = parameters.getPath().segment(fieldName);
ExecutionStrategyParameters newParameters = parameters
.transform(builder -> builder.field(currentField).path(fieldPath));

return Async.<FetchedValue>toCompletableFuture(fetchField(executionContext, newParameters)).join();
return Async.toCompletableFuture(fetchField(executionContext, newParameters)).join();
}

private void completeValue(ExecutionContext executionContext, Map<String, Object> results, String fieldName, FetchedValue fetchedValue, ExecutionStrategyParameters newParameters) {
private void completeValue(ExecutionContext executionContext, Map<String, Object> results, String fieldName, Object fetchedValue, ExecutionStrategyParameters newParameters) {
Object resolvedResult = completeField(executionContext, newParameters, fetchedValue).getFieldValueFuture().join();
results.put(fieldName, resolvedResult);
}
Expand Down
Loading