diff --git a/gradle.properties b/gradle.properties index 5f0badb9fd..63cffdd772 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ org.gradle.daemon=true bintray.user=DUMMY_USER -bintray.key=DUMMY_KEY \ No newline at end of file +bintray.key=DUMMY_KEY + diff --git a/src/main/java/graphql/ExceptionWhileDataFetching.java b/src/main/java/graphql/ExceptionWhileDataFetching.java index a97c7bdda6..8b08fc6b45 100644 --- a/src/main/java/graphql/ExceptionWhileDataFetching.java +++ b/src/main/java/graphql/ExceptionWhileDataFetching.java @@ -1,15 +1,22 @@ package graphql; +import graphql.execution.Path; import graphql.language.SourceLocation; import java.util.List; public class ExceptionWhileDataFetching implements GraphQLError { + private final Path path; private final Throwable exception; public ExceptionWhileDataFetching(Throwable exception) { + this(null, exception); + } + + public ExceptionWhileDataFetching(Path path, Throwable exception) { + this.path = path; this.exception = exception; } @@ -28,6 +35,10 @@ public List getLocations() { return null; } + public String getPath() { + return path == null ? null : path.toString(); + } + @Override public ErrorType getErrorType() { return ErrorType.DataFetchingException; diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 17abdfc830..61f1518a14 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -55,9 +55,9 @@ private ExecutionResult executeOperation( fieldCollector.collectFields(executionContext, operationRootType, operationDefinition.getSelectionSet(), new ArrayList(), fields); if (operationDefinition.getOperation() == OperationDefinition.Operation.MUTATION) { - return mutationStrategy.execute(executionContext, operationRootType, root, fields); + return mutationStrategy.execute(executionContext, Path.root, operationRootType, root, fields); } else { - return queryStrategy.execute(executionContext, operationRootType, root, fields); + return queryStrategy.execute(executionContext, Path.root, operationRootType, root, fields); } } } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 5be0ed176d..785d19cec8 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -19,9 +19,9 @@ public abstract class ExecutionStrategy { protected final ValuesResolver valuesResolver = new ValuesResolver(); protected final FieldCollector fieldCollector = new FieldCollector(); - public abstract ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields); + public abstract ExecutionResult execute(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, Object source, Map> fields); - protected ExecutionResult resolveField(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, List fields) { + protected ExecutionResult resolveField(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, Object source, List fields) { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, fields.get(0)); Map argumentValues = valuesResolver.getArgumentValues(fieldDef.getArguments(), fields.get(0).getArguments(), executionContext.getVariables()); @@ -40,16 +40,16 @@ protected ExecutionResult resolveField(ExecutionContext executionContext, GraphQ resolvedValue = fieldDef.getDataFetcher().get(environment); } catch (Exception e) { log.warn("Exception while fetching data", e); - executionContext.addError(new ExceptionWhileDataFetching(e)); + executionContext.addError(new ExceptionWhileDataFetching(currentPath, e)); } - return completeValue(executionContext, fieldDef.getType(), fields, resolvedValue); + return completeValue(executionContext, currentPath, fieldDef.getType(), fields, resolvedValue); } - protected ExecutionResult completeValue(ExecutionContext executionContext, GraphQLType fieldType, List fields, Object result) { + protected ExecutionResult completeValue(ExecutionContext executionContext, Path currentPath, GraphQLType fieldType, List fields, Object result) { if (fieldType instanceof GraphQLNonNull) { GraphQLNonNull graphQLNonNull = (GraphQLNonNull) fieldType; - ExecutionResult completed = completeValue(executionContext, graphQLNonNull.getWrappedType(), fields, result); + ExecutionResult completed = completeValue(executionContext, currentPath, graphQLNonNull.getWrappedType(), fields, result); if (completed == null) { throw new GraphQLException("Cannot return null for non-nullable type: " + fields); } @@ -58,7 +58,7 @@ protected ExecutionResult completeValue(ExecutionContext executionContext, Graph } else if (result == null) { return null; } else if (fieldType instanceof GraphQLList) { - return completeValueForList(executionContext, (GraphQLList) fieldType, fields, result); + return completeValueForList(executionContext, currentPath, (GraphQLList) fieldType, fields, result); } else if (fieldType instanceof GraphQLScalarType) { return completeValueForScalar((GraphQLScalarType) fieldType, result); } else if (fieldType instanceof GraphQLEnumType) { @@ -84,16 +84,16 @@ protected ExecutionResult completeValue(ExecutionContext executionContext, Graph // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. - return executionContext.getQueryStrategy().execute(executionContext, resolvedType, result, subFields); + return executionContext.getQueryStrategy().execute(executionContext, currentPath, resolvedType, result, subFields); } - private ExecutionResult completeValueForList(ExecutionContext executionContext, GraphQLList fieldType, List fields, Object result) { + private ExecutionResult completeValueForList(ExecutionContext executionContext, Path currentPath, GraphQLList fieldType, List fields, Object result) { if (result.getClass().isArray()) { result = Arrays.asList((Object[]) result); } //noinspection unchecked - return completeValueForList(executionContext, fieldType, fields, (Iterable) result); + return completeValueForList(executionContext, currentPath, fieldType, fields, (Iterable) result); } protected GraphQLObjectType resolveType(GraphQLInterfaceType graphQLInterfaceType, Object value) { @@ -126,10 +126,12 @@ protected ExecutionResult completeValueForScalar(GraphQLScalarType scalarType, O return new ExecutionResultImpl(serialized, null); } - protected ExecutionResult completeValueForList(ExecutionContext executionContext, GraphQLList fieldType, List fields, Iterable result) { + protected ExecutionResult completeValueForList(ExecutionContext executionContext, Path currentPath, GraphQLList fieldType, List fields, Iterable result) { List completedResults = new ArrayList(); + int idx = -1; for (Object item : result) { - ExecutionResult completedValue = completeValue(executionContext, fieldType.getWrappedType(), fields, item); + ++idx; + ExecutionResult completedValue = completeValue(executionContext, currentPath.withTrailing(idx), fieldType.getWrappedType(), fields, item); completedResults.add(completedValue != null ? completedValue.getData() : null); } return new ExecutionResultImpl(completedResults, null); @@ -154,6 +156,4 @@ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObject } return fieldDefinition; } - - } diff --git a/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java b/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java index b0043e819e..be176613a7 100644 --- a/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java @@ -17,7 +17,7 @@ /** *

ExecutorServiceExecutionStrategy uses an {@link ExecutorService} to parallelize the resolve.

* - * Due to the nature of {@link #execute(ExecutionContext, GraphQLObjectType, Object, Map)} implementation, {@link ExecutorService} + * Due to the nature of {@link #execute(ExecutionContext, Path, GraphQLObjectType, Object, Map)} implementation, {@link ExecutorService} * MUST have the following 2 characteristics: *
    *
  • 1. The underlying {@link java.util.concurrent.ThreadPoolExecutor} MUST have a reasonable {@code maximumPoolSize} @@ -37,17 +37,17 @@ public ExecutorServiceExecutionStrategy(ExecutorService executorService) { } @Override - public ExecutionResult execute(final ExecutionContext executionContext, final GraphQLObjectType parentType, final Object source, final Map> fields) { + public ExecutionResult execute(final ExecutionContext executionContext, final Path currentPath, final GraphQLObjectType parentType, final Object source, final Map> fields) { if (executorService == null) - return new SimpleExecutionStrategy().execute(executionContext, parentType, source, fields); + return new SimpleExecutionStrategy().execute(executionContext, currentPath, parentType, source, fields); Map> futures = new LinkedHashMap>(); - for (String fieldName : fields.keySet()) { + for (final String fieldName : fields.keySet()) { final List fieldList = fields.get(fieldName); Callable resolveField = new Callable() { @Override public ExecutionResult call() throws Exception { - return resolveField(executionContext, parentType, source, fieldList); + return resolveField(executionContext, currentPath.withTrailing(fieldName), parentType, source, fieldList); } }; diff --git a/src/main/java/graphql/execution/Path.java b/src/main/java/graphql/execution/Path.java new file mode 100644 index 0000000000..c764387eb0 --- /dev/null +++ b/src/main/java/graphql/execution/Path.java @@ -0,0 +1,71 @@ +package graphql.execution; + +public class Path { + private final Path parent; + private PathComponent tail; + + public static Path root= new Path(); + + private Path() { + parent = null; + tail = null; + } + + public Path(String topLevelField) { + this(null, new StringPathComponent(topLevelField)); + } + + private Path(Path parent, PathComponent tail) { + this.parent = parent; + this.tail = tail; + } + + public Path withTrailing(String trailing) { + return new Path(this, new StringPathComponent(trailing)); + } + + public Path withTrailing(int index) { + return new Path(this, new IntPathComponent(index)); + } + + @Override + public String toString() { + if (tail == null) { + return ""; + } + + if (parent == root) { + return tail.toString().substring(1); + } + + return parent.toString() + tail.toString(); + } + + private interface PathComponent { + } + + private static class StringPathComponent implements PathComponent { + private final String value; + public StringPathComponent(String value) { + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException("empty path component"); + } + this.value = value; + } + @Override + public String toString() { + return '/' + value; + } + } + + private static class IntPathComponent implements PathComponent { + private final int value; + public IntPathComponent(int value) { + this.value = value; + } + @Override + public String toString() { + return "[" + value + ']'; + } + } +} diff --git a/src/main/java/graphql/execution/SimpleExecutionStrategy.java b/src/main/java/graphql/execution/SimpleExecutionStrategy.java index 25cb2718b3..5235320d41 100644 --- a/src/main/java/graphql/execution/SimpleExecutionStrategy.java +++ b/src/main/java/graphql/execution/SimpleExecutionStrategy.java @@ -11,11 +11,11 @@ public class SimpleExecutionStrategy extends ExecutionStrategy { @Override - public ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + public ExecutionResult execute(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, Object source, Map> fields) { Map results = new LinkedHashMap(); for (String fieldName : fields.keySet()) { List fieldList = fields.get(fieldName); - ExecutionResult resolvedResult = resolveField(executionContext, parentType, source, fieldList); + ExecutionResult resolvedResult = resolveField(executionContext, currentPath.withTrailing(fieldName), parentType, source, fieldList); results.put(fieldName, resolvedResult != null ? resolvedResult.getData() : null); } diff --git a/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java b/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java index 5c2557295d..9021e162fa 100644 --- a/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java +++ b/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java @@ -6,6 +6,7 @@ import graphql.GraphQLException; import graphql.execution.ExecutionContext; import graphql.execution.ExecutionStrategy; +import graphql.execution.Path; import graphql.language.Field; import graphql.schema.*; import org.slf4j.Logger; @@ -32,13 +33,13 @@ public class BatchedExecutionStrategy extends ExecutionStrategy { private final BatchedDataFetcherFactory batchingFactory = new BatchedDataFetcherFactory(); @Override - public ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + public ExecutionResult execute(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, Object source, Map> fields) { GraphQLExecutionNodeDatum data = new GraphQLExecutionNodeDatum(new LinkedHashMap(), source); GraphQLExecutionNode root = new GraphQLExecutionNode(parentType, fields, singletonList(data)); - return execute(executionContext, root); + return execute(executionContext, currentPath, root); } - private ExecutionResult execute(ExecutionContext executionContext, GraphQLExecutionNode root) { + private ExecutionResult execute(ExecutionContext executionContext, Path currentPath, GraphQLExecutionNode root) { Queue nodes = new ArrayDeque(); nodes.add(root); @@ -49,7 +50,7 @@ private ExecutionResult execute(ExecutionContext executionContext, GraphQLExecut for (String fieldName : node.getFields().keySet()) { List fieldList = node.getFields().get(fieldName); - List childNodes = resolveField(executionContext, node.getParentType(), + List childNodes = resolveField(executionContext, currentPath.withTrailing(fieldName), node.getParentType(), node.getData(), fieldName, fieldList); nodes.addAll(childNodes); } @@ -66,14 +67,14 @@ private GraphQLExecutionNodeDatum getOnlyElement(List // Use the data.parentResult objects to put values into. These are either primitives or empty maps // If they were empty maps, we need that list of nodes to process - private List resolveField(ExecutionContext executionContext, GraphQLObjectType parentType, + private List resolveField(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, List nodeData, String fieldName, List fields) { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, fields.get(0)); if (fieldDef == null) { return Collections.emptyList(); } - List values = fetchData(executionContext, parentType, nodeData, fields, fieldDef); + List values = fetchData(executionContext, currentPath, parentType, nodeData, fields, fieldDef); return completeValues(executionContext, parentType, values, fieldName, fields, fieldDef.getType()); } @@ -242,7 +243,7 @@ private boolean isObject(GraphQLType type) { } @SuppressWarnings("unchecked") - private List fetchData(ExecutionContext executionContext, GraphQLObjectType parentType, + private List fetchData(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, List nodeData, List fields, GraphQLFieldDefinition fieldDef) { Map argumentValues = valuesResolver.getArgumentValues( @@ -270,7 +271,7 @@ private List fetchData(ExecutionContext executionCont values.add(null); } log.warn("Exception while fetching data", e); - executionContext.addError(new ExceptionWhileDataFetching(e)); + executionContext.addError(new ExceptionWhileDataFetching(currentPath, e)); } assert nodeData.size() == values.size(); diff --git a/src/test/groovy/graphql/ErrorPathTest.java b/src/test/groovy/graphql/ErrorPathTest.java new file mode 100644 index 0000000000..c116064e3a --- /dev/null +++ b/src/test/groovy/graphql/ErrorPathTest.java @@ -0,0 +1,83 @@ +package graphql; + +import static graphql.Scalars.GraphQLString; +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; +import static graphql.schema.GraphQLObjectType.newObject; +import static org.junit.Assert.assertEquals; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class ErrorPathTest { + @Test + public void testErrorPath() { + GraphQLObjectType queryType = newObject() + .name("errorPathQuery") + .field(newFieldDefinition() + .name("f1") + .type(GraphQLString) + .dataFetcher(new DataFetcher(){ + @Override + public Object get(DataFetchingEnvironment environment) { + throw new RuntimeException("error."); + } + }) + .build()) + .field(newFieldDefinition() + .name("f2") + .type(new GraphQLList(newObject() + .name("Test") + .field(newFieldDefinition() + .name("sub1") + .type(GraphQLString) + .staticValue("test") + .build() + ) + .field(newFieldDefinition() + .name("sub2") + .type(GraphQLString) + .dataFetcher(new DataFetcher(){ + @Override + public Object get(DataFetchingEnvironment environment) { + boolean willThrow = (Boolean) environment.getSource(); + if (willThrow) { + throw new RuntimeException("error."); + } + return "no error"; + } + }) + .build() + ) + .build() + )) + .dataFetcher(new DataFetcher() { + @Override + public Object get(DataFetchingEnvironment environment) { + return new Boolean[] { false, true, false}; + } + }) + .build() + ) + .build(); + + GraphQLSchema schema = GraphQLSchema.newSchema() + .query(queryType) + .build(); + + GraphQL graphQL = GraphQL.newGraphQL(schema).build(); + + List errors = graphQL.execute("{f1 f2{sub1 sub2}}").getErrors(); + + ExceptionWhileDataFetching error = (ExceptionWhileDataFetching) errors.get(0); + assertEquals("f1", error.getPath()); + + error = (ExceptionWhileDataFetching) errors.get(1); + assertEquals("f2[1]/sub2", error.getPath()); + } +} diff --git a/src/test/groovy/graphql/execution/ExecutionIdTest.groovy b/src/test/groovy/graphql/execution/ExecutionIdTest.groovy index 5471d56793..3488407938 100644 --- a/src/test/groovy/graphql/execution/ExecutionIdTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionIdTest.groovy @@ -13,9 +13,9 @@ class ExecutionIdTest extends Specification { ExecutionId executionId = null @Override - ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + ExecutionResult execute(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, Object source, Map> fields) { executionId = executionContext.executionId - return super.execute(executionContext, parentType, source, fields) + return super.execute(executionContext, currentPath, parentType, source, fields) } } diff --git a/src/test/groovy/graphql/execution/ExecutionStrategySpec.groovy b/src/test/groovy/graphql/execution/ExecutionStrategySpec.groovy index a6fe80c2cd..bc5605f16e 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategySpec.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategySpec.groovy @@ -14,7 +14,7 @@ class ExecutionStrategySpec extends Specification { def setup() { executionStrategy = new ExecutionStrategy() { @Override - ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + ExecutionResult execute(ExecutionContext executionContext, Path currentPath, GraphQLObjectType parentType, Object source, Map> fields) { return null } } @@ -31,7 +31,7 @@ class ExecutionStrategySpec extends Specification { def fieldType = new GraphQLList(Scalars.GraphQLString) def result = Arrays.asList("test") when: - def executionResult = executionStrategy.completeValue(executionContext, fieldType, [field], result) + def executionResult = executionStrategy.completeValue(executionContext, Path.root, fieldType, [field], result) then: executionResult.data == ["test"] @@ -44,7 +44,7 @@ class ExecutionStrategySpec extends Specification { def fieldType = new GraphQLList(Scalars.GraphQLString) String[] result = ["test"] when: - def executionResult = executionStrategy.completeValue(executionContext, fieldType, [field], result) + def executionResult = executionStrategy.completeValue(executionContext, Path.root, fieldType, [field], result) then: executionResult.data == ["test"]