Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/main/java/graphql/execution/DataFetcherResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@

import com.google.common.collect.ImmutableList;
import graphql.DeprecatedAt;
import graphql.ExecutionResult;
import graphql.GraphQLError;
import graphql.Internal;
import graphql.PublicApi;
import graphql.schema.DataFetcher;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import static graphql.Assert.assertNotNull;


/**
* An object that can be returned from a {@link DataFetcher} that contains both data, local context and errors to be relativized and
* added to the final result. This is a useful when your ``DataFetcher`` retrieves data from multiple sources
* or from another GraphQL resource or you want to pass extra context to lower levels.
*
* An object that can be returned from a {@link DataFetcher} that contains both data, local context and errors to be added to the final result.
* This is a useful when your ``DataFetcher`` retrieves data from multiple sources
* or from another GraphQL resource, or you want to pass extra context to lower levels.
* <p>
* This also allows you to pass down new local context objects between parent and child fields. If you return a
* {@link #getLocalContext()} value then it will be passed down into any child fields via
* {@link graphql.schema.DataFetchingEnvironment#getLocalContext()}
*
* You can also have {@link DataFetcher}s contribute to the {@link ExecutionResult#getExtensions()} by returning
* extensions maps that will be merged together via the {@link graphql.extensions.ExtensionsBuilder} and its {@link graphql.extensions.ExtensionsMerger}
* in place.
*
* @param <T> The type of the data fetched
*/
@PublicApi
Expand All @@ -31,6 +37,7 @@ public class DataFetcherResult<T> {
private final T data;
private final List<GraphQLError> errors;
private final Object localContext;
private final Map<Object, Object> extensions;

/**
* Creates a data fetcher result
Expand All @@ -44,13 +51,14 @@ public class DataFetcherResult<T> {
@Deprecated
@DeprecatedAt("2019-01-11")
public DataFetcherResult(T data, List<GraphQLError> errors) {
this(data, errors, null);
this(data, errors, null, null);
}

private DataFetcherResult(T data, List<GraphQLError> errors, Object localContext) {
private DataFetcherResult(T data, List<GraphQLError> errors, Object localContext, Map<Object, Object> extensions) {
this.data = data;
this.errors = ImmutableList.copyOf(assertNotNull(errors));
this.localContext = localContext;
this.extensions = extensions;
}

/**
Expand Down Expand Up @@ -83,6 +91,22 @@ public Object getLocalContext() {
return localContext;
}

/**
* A data fetcher result can supply extension values that will be merged into the result
* via the {@link graphql.extensions.ExtensionsBuilder} at the end of the operation.
* <p>
* The {@link graphql.extensions.ExtensionsMerger} in place inside the {@link graphql.extensions.ExtensionsBuilder}
* will control how these extension values get merged.
*
* @return a map of extension values to be merged
*
* @see graphql.extensions.ExtensionsBuilder
* @see graphql.extensions.ExtensionsMerger
*/
public Map<Object, Object> getExtensions() {
return extensions;
}

/**
* This helps you transform the current DataFetcherResult into another one by starting a builder with all
* the current values and allows you to transform it how you want.
Expand Down Expand Up @@ -112,11 +136,13 @@ public static class Builder<T> {
private T data;
private Object localContext;
private final List<GraphQLError> errors = new ArrayList<>();
private Map<Object, Object> extensions;

public Builder(DataFetcherResult<T> existing) {
data = existing.getData();
localContext = existing.getLocalContext();
errors.addAll(existing.getErrors());
extensions = existing.extensions;
}

public Builder(T data) {
Expand Down Expand Up @@ -158,8 +184,13 @@ public Builder<T> localContext(Object localContext) {
return this;
}

public Builder<T> extensions(Map<Object, Object> extensions) {
this.extensions = extensions;
return this;
}

public DataFetcherResult<T> build() {
return new DataFetcherResult<>(data, errors, localContext);
return new DataFetcherResult<>(data, errors, localContext, extensions);
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/graphql/execution/ExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import graphql.execution.instrumentation.parameters.InstrumentationFieldCompleteParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters;
import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters;
import graphql.extensions.ExtensionsBuilder;
import graphql.introspection.Introspection;
import graphql.language.Argument;
import graphql.language.Field;
Expand Down Expand Up @@ -330,6 +331,7 @@ protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext execution
if (result instanceof DataFetcherResult) {
DataFetcherResult<?> dataFetcherResult = (DataFetcherResult<?>) result;
executionContext.addErrors(dataFetcherResult.getErrors());
addExtensionsIfPresent(executionContext,dataFetcherResult);

Object localContext = dataFetcherResult.getLocalContext();
if (localContext == null) {
Expand All @@ -351,6 +353,16 @@ protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext execution
}
}

private void addExtensionsIfPresent(ExecutionContext executionContext, DataFetcherResult<?> dataFetcherResult) {
Map<Object, Object> extensions = dataFetcherResult.getExtensions();
if (extensions != null) {
ExtensionsBuilder extensionsBuilder = executionContext.getGraphQLContext().get(ExtensionsBuilder.class);
if (extensionsBuilder != null) {
extensionsBuilder.addValues(extensions);
}
}
}

protected <T> CompletableFuture<T> handleFetchingException(ExecutionContext executionContext,
DataFetchingEnvironment environment,
Throwable e) {
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/graphql/extensions/ExtensionsBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ public static ExtensionsBuilder newExtensionsBuilder(ExtensionsMerger extensions
return new ExtensionsBuilder(extensionsMerger);
}

/**
* @return how many extension changes have been made so far
*/
public int getChangeCount() {
return changes.size();
}

/**
* Adds new values into the extension builder
Expand All @@ -65,7 +71,9 @@ public static ExtensionsBuilder newExtensionsBuilder(ExtensionsMerger extensions
*/
public ExtensionsBuilder addValues(@NotNull Map<Object, Object> newValues) {
assertNotNull(newValues);
changes.add(newValues);
if (!newValues.isEmpty()) {
changes.add(newValues);
}
return this;
}

Expand Down
33 changes: 31 additions & 2 deletions src/test/groovy/graphql/execution/DataFetcherResultTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,43 @@ class DataFetcherResultTest extends Specification {
!result.hasErrors()
}

def "transforming"() {
def "can set extensions"() {

when:
def dfr = DataFetcherResult.newResult()
.extensions([x: "y"]).build()

then:
dfr.getExtensions() == [x : "y"]

when:
dfr = DataFetcherResult.newResult()
.data("x")
.build()

then:
dfr.getExtensions() == null

}

def "transforming works"() {
when:
def original = DataFetcherResult.newResult().data("hello")
.errors([error1]).localContext("world").build()
.errors([error1]).localContext("world")
.extensions([x: "y"]).build()
def result = original.transform({ builder -> builder.error(error2) })
then:
result.getData() == "hello"
result.getLocalContext() == "world"
result.getExtensions() == [x : "y"]
result.getErrors() == [error1, error2]

when:
result = result.transform({ builder -> builder.extensions(a : "b") })
then:
result.getData() == "hello"
result.getLocalContext() == "world"
result.getExtensions() == [a : "b"]
result.getErrors() == [error1, error2]
}
}
80 changes: 72 additions & 8 deletions src/test/groovy/graphql/extensions/ExtensionsBuilderTest.groovy
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package graphql.extensions

import graphql.ExecutionInput
import graphql.ExecutionResult
import graphql.TestUtil
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import graphql.schema.GraphQLTypeUtil
import org.jetbrains.annotations.NotNull
import spock.lang.Specification

import static graphql.ExecutionInput.newExecutionInput
import static graphql.extensions.ExtensionsBuilder.newExtensionsBuilder
import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring
Expand Down Expand Up @@ -39,6 +40,27 @@ class ExtensionsBuilderTest extends Specification {
extensions == [x: "overwrite3", y: "25", z: "overwriteZ", a: "1"]
}

def "wont add empty changes"() {
def builder = newExtensionsBuilder()
when:
builder.addValues([:])

then:
builder.getChangeCount() == 0

when:
builder.addValues([:])

then:
builder.getChangeCount() == 0

when:
def extensions = builder.buildExtensions()
then:
extensions.isEmpty()

}

def "can handle no changes"() {
when:
def extensions = newExtensionsBuilder()
Expand Down Expand Up @@ -122,11 +144,12 @@ class ExtensionsBuilderTest extends Specification {
"""

def extensionsBuilder = newExtensionsBuilder()
extensionsBuilder.addValue("added","explicitly")
extensionsBuilder.addValue("added", "explicitly")

def ei = ExecutionInput.newExecutionInput("query q { name street id }")
def ei = newExecutionInput("query q { name street id }")
.graphQLContext({ ctx ->
ctx.put(ExtensionsBuilder.class, extensionsBuilder) })
ctx.put(ExtensionsBuilder.class, extensionsBuilder)
})
.build()


Expand All @@ -144,12 +167,53 @@ class ExtensionsBuilderTest extends Specification {
er.errors.isEmpty()
er.extensions == [
"added": "explicitly",
common: [
common : [
name : "String!",
street: "String",
id : "ID!",
],
// we break them out so we have common and not common entries
name : "String!",
street : "String",
id : "ID!",
]
}


def "integration test that shows it working when they use DataFetcherResult and defaulted values"() {
def sdl = """
type Query {
name : String!
street : String
id : ID!
}
"""

DataFetcher dfrDF = new DataFetcher() {
@Override
Object get(DataFetchingEnvironment env) throws Exception {
def fieldMap = [:]
fieldMap.put(env.getFieldDefinition().name, GraphQLTypeUtil.simplePrint(env.getFieldDefinition().type))
return DataFetcherResult.newResult().data("ignored").extensions(fieldMap).build()
}
}

def graphQL = TestUtil.graphQL(sdl, newRuntimeWiring()
.type(newTypeWiring("Query").dataFetchers([
name : dfrDF,
street: dfrDF,
id : dfrDF,
])))
.build()

when:
def ei = newExecutionInput("query q { name street id }")
.build()

def er = graphQL.execute(ei)
then:
er.errors.isEmpty()
er.extensions == [
name : "String!",
street: "String",
id : "ID!",
Expand All @@ -165,7 +229,7 @@ class ExtensionsBuilderTest extends Specification {
}
"""

def ei = ExecutionInput.newExecutionInput("query q { name street id }")
def ei = newExecutionInput("query q { name street id }")
.build()


Expand Down Expand Up @@ -203,8 +267,8 @@ class ExtensionsBuilderTest extends Specification {
}
"""

def ei = ExecutionInput.newExecutionInput("query q { name street id }")
.root(["name" : "Brad", "id" :1234])
def ei = newExecutionInput("query q { name street id }")
.root(["name": "Brad", "id": 1234])
.build()


Expand Down