diff --git a/build.gradle b/build.gradle index 373fbee8db..7239cbe48e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,3 @@ -import java.text.SimpleDateFormat - plugins { id "com.jfrog.bintray" version "1.2" } @@ -10,10 +8,11 @@ apply plugin: 'maven-publish' apply plugin: 'antlr' apply plugin: 'jacoco' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 def releaseVersion = System.properties.RELEASE_VERSION -version = releaseVersion ? releaseVersion : new SimpleDateFormat('yyyy-MM-dd\'T\'HH-mm-ss').format(new Date()) +version = "3.0.0-SNAPSHOT" +//version = releaseVersion ? releaseVersion : new SimpleDateFormat('yyyy-MM-dd\'T\'HH-mm-ss').format(new Date()) group = 'com.graphql-java' @@ -30,12 +29,14 @@ jar { dependencies { compile 'org.antlr:antlr4-runtime:4.5.1' compile 'org.slf4j:slf4j-api:1.7.12' + compile 'com.spotify:completable-futures:0.3.0' antlr "org.antlr:antlr4:4.5.1" testCompile group: 'junit', name: 'junit', version: '4.11' testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' testCompile 'org.codehaus.groovy:groovy-all:2.4.4' testCompile 'cglib:cglib-nodep:3.1' testCompile 'org.objenesis:objenesis:2.1' + testCompile 'org.slf4j:slf4j-log4j12:1.7.21' } compileJava.source file("build/generated-src"), sourceSets.main.java diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index aca02821ef..405a91a2ed 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletionStage; import static graphql.Assert.assertNotNull; @@ -34,11 +35,10 @@ public ExecutionId provide(String query, String operationName, Object context) { } }; - private final GraphQLSchema graphQLSchema; - private final ExecutionStrategy queryStrategy; - private final ExecutionStrategy mutationStrategy; - private final ExecutionIdProvider idProvider; - + protected final GraphQLSchema graphQLSchema; + protected final ExecutionStrategy queryStrategy; + protected final ExecutionStrategy mutationStrategy; + protected final ExecutionIdProvider idProvider; /** * A GraphQL object ready to execute queries diff --git a/src/main/java/graphql/GraphQLAsync.java b/src/main/java/graphql/GraphQLAsync.java new file mode 100644 index 0000000000..e0e7e2c90a --- /dev/null +++ b/src/main/java/graphql/GraphQLAsync.java @@ -0,0 +1,87 @@ +package graphql; + +import graphql.execution.AsyncExecution; +import graphql.execution.Execution; +import graphql.execution.ExecutionStrategy; +import graphql.execution.async.AsyncExecutionStrategy; +import graphql.language.Document; +import graphql.language.SourceLocation; +import graphql.parser.Parser; +import graphql.schema.GraphQLSchema; +import graphql.validation.ValidationError; +import graphql.validation.Validator; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +import static graphql.Assert.assertNotNull; +import static java.util.concurrent.CompletableFuture.completedFuture; + +public class GraphQLAsync extends GraphQL { + + private static Logger log = LoggerFactory.getLogger(GraphQLAsync.class); + + public GraphQLAsync(GraphQLSchema graphQLSchema) { + this(graphQLSchema, AsyncExecutionStrategy.parallel()); + } + + public GraphQLAsync(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) { + this(graphQLSchema, queryStrategy, AsyncExecutionStrategy.serial()); + } + + public GraphQLAsync(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { + super(graphQLSchema, queryStrategy, mutationStrategy); + } + + public CompletionStage executeAsync(String requestString) { + return executeAsync(requestString, null); + + } + + public CompletionStage executeAsync(String requestString, Object context) { + return executeAsync(requestString, context, Collections.emptyMap()); + + } + + public CompletionStage executeAsync(String requestString, String operationName, Object context) { + return executeAsync(requestString, operationName, context, Collections.emptyMap()); + + } + + public CompletionStage executeAsync(String requestString, Object context, Map arguments) { + return executeAsync(requestString, null, context, arguments); + + } + + @SuppressWarnings("unchecked") + public CompletionStage executeAsync(String requestString, String operationName, Object context, Map arguments) { + + assertNotNull(arguments, "arguments can't be null"); + log.debug("Executing request. operation name: {}. Request: {} ", operationName, requestString); + Parser parser = new Parser(); + Document document; + try { + document = parser.parseDocument(requestString); + } catch (ParseCancellationException e) { + RecognitionException recognitionException = (RecognitionException) e.getCause(); + SourceLocation sourceLocation = new SourceLocation(recognitionException.getOffendingToken().getLine(), recognitionException.getOffendingToken().getCharPositionInLine()); + InvalidSyntaxError invalidSyntaxError = new InvalidSyntaxError(sourceLocation); + return completedFuture(new ExecutionResultImpl(Arrays.asList(invalidSyntaxError))); + } + + Validator validator = new Validator(); + List validationErrors = validator.validateDocument(graphQLSchema, document); + if (validationErrors.size() > 0) { + return completedFuture(new ExecutionResultImpl(validationErrors)); + } + AsyncExecution execution = new AsyncExecution(queryStrategy, mutationStrategy); + return execution.executeAsync(graphQLSchema, context, document, operationName, arguments); + } +} diff --git a/src/main/java/graphql/NonNullException.java b/src/main/java/graphql/NonNullException.java new file mode 100644 index 0000000000..33ecf61cc3 --- /dev/null +++ b/src/main/java/graphql/NonNullException.java @@ -0,0 +1,8 @@ +package graphql; + +public class NonNullException extends GraphQLException { + + public NonNullException(String s) { + super(s); + } +} diff --git a/src/main/java/graphql/execution/AsyncExecution.java b/src/main/java/graphql/execution/AsyncExecution.java new file mode 100644 index 0000000000..cba15da0c7 --- /dev/null +++ b/src/main/java/graphql/execution/AsyncExecution.java @@ -0,0 +1,46 @@ +package graphql.execution; + +import graphql.ExecutionResult; +import graphql.execution.async.AsyncExecutionStrategy; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.OperationDefinition; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +public class AsyncExecution extends Execution { + + public AsyncExecution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { + super(queryStrategy, mutationStrategy); + } + + public CompletionStage executeAsync(GraphQLSchema graphQLSchema, Object root, Document document, String operationName, Map args) { + ExecutionContextBuilder executionContextBuilder = new ExecutionContextBuilder(new ValuesResolver()); + ExecutionContext executionContext = executionContextBuilder + .executionId(ExecutionId.generate()) + .build(graphQLSchema, queryStrategy, mutationStrategy, root, document, operationName, args); + return executeOperationAsync(executionContext, root, executionContext.getOperationDefinition()); + } + + private CompletionStage executeOperationAsync( + ExecutionContext executionContext, + Object root, + OperationDefinition operationDefinition) { + GraphQLObjectType operationRootType = getOperationRootType(executionContext.getGraphQLSchema(), operationDefinition); + + Map> fields = new LinkedHashMap>(); + fieldCollector.collectFields(executionContext, operationRootType, operationDefinition.getSelectionSet(), new ArrayList(), fields); + + if (operationDefinition.getOperation() == OperationDefinition.Operation.MUTATION) { + return ((AsyncExecutionStrategy) mutationStrategy).executeAsync(executionContext, operationRootType, root, fields); + } else { + return ((AsyncExecutionStrategy) queryStrategy).executeAsync(executionContext, operationRootType, root, fields); + } + } +} diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 17abdfc830..f80b123992 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -3,6 +3,7 @@ import graphql.ExecutionResult; import graphql.GraphQLException; +import graphql.execution.async.AsyncExecutionStrategy; import graphql.language.Document; import graphql.language.Field; import graphql.language.OperationDefinition; @@ -13,12 +14,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.stream.Stream; public class Execution { - private FieldCollector fieldCollector = new FieldCollector(); - private ExecutionStrategy queryStrategy; - private ExecutionStrategy mutationStrategy; + protected FieldCollector fieldCollector = new FieldCollector(); + protected ExecutionStrategy queryStrategy; + protected ExecutionStrategy mutationStrategy; public Execution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { this.queryStrategy = queryStrategy != null ? queryStrategy : new SimpleExecutionStrategy(); @@ -33,7 +36,7 @@ public ExecutionResult execute(ExecutionId executionId, GraphQLSchema graphQLSch return executeOperation(executionContext, root, executionContext.getOperationDefinition()); } - private GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) { + protected GraphQLObjectType getOperationRootType(GraphQLSchema graphQLSchema, OperationDefinition operationDefinition) { if (operationDefinition.getOperation() == OperationDefinition.Operation.MUTATION) { return graphQLSchema.getMutationType(); diff --git a/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java new file mode 100644 index 0000000000..675ae22954 --- /dev/null +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -0,0 +1,224 @@ +package graphql.execution.async; + +import com.spotify.futures.CompletableFutures; +import graphql.*; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStrategy; +import graphql.language.Field; +import graphql.schema.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Objects.isNull; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; + + +public final class AsyncExecutionStrategy extends ExecutionStrategy { + + public static AsyncExecutionStrategy serial() { + return new AsyncExecutionStrategy(true, null); + } + + public static AsyncExecutionStrategy serial(final CompletableFutureFactory factory) { + return new AsyncExecutionStrategy(true, factory); + } + + public static AsyncExecutionStrategy parallel() { + return new AsyncExecutionStrategy(false, null); + } + + public static AsyncExecutionStrategy parallel(final CompletableFutureFactory factory) { + return new AsyncExecutionStrategy(false, factory); + } + + private static final Logger log = LoggerFactory.getLogger(AsyncExecutionStrategy.class); + + private final boolean serial; + private final CompletableFutureFactory completableFutureFactory; + + private AsyncExecutionStrategy(boolean serial, final CompletableFutureFactory completableFutureFactory) { + this.serial = serial; + if (isNull(completableFutureFactory)) { + this.completableFutureFactory = DefaultCompletableFutureFactory.defaultFactory(); + } else { + this.completableFutureFactory = completableFutureFactory; + } + } + + @Override + public ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + try { + return executeAsync(executionContext, parentType, source, fields).toCompletableFuture().get(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public CompletionStage executeAsync(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + + Map>> fieldsToExecute = fields.keySet() + .stream() + .collect(Collectors.toMap( + Function.identity(), + field -> () -> resolveFieldAsync(executionContext, parentType, source, fields.get(field)), + (a, b) -> a, + LinkedHashMap::new + )); + + return executeFields(fieldsToExecute).thenApply(resultMap -> { + Map dataMap = new HashMap<>(); + resultMap.forEach((key, result) -> { + dataMap.put(key, result.getData()); + }); + return new ExecutionResultImpl(dataMap, executionContext.getErrors()); + }).exceptionally(e -> { + if (e.getCause() instanceof NonNullException) { + return new ExecutionResultImpl(null, null); + } + throw new RuntimeException(e); + }); + } + + protected CompletionStage resolveFieldAsync(ExecutionContext executionContext, 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()); + DataFetchingEnvironment environment = new DataFetchingEnvironmentImpl( + source, + argumentValues, + executionContext.getRoot(), + fields, + fieldDef.getType(), + parentType, + executionContext.getGraphQLSchema() + ); + + CompletionStage stage; + try { + Object resolvedValue = fieldDef.getDataFetcher().get(environment); + stage = resolvedValue instanceof CompletionStage ? (CompletionStage) resolvedValue : completedFuture(resolvedValue); + } catch (Exception e) { + log.warn("Exception while fetching data", e); + executionContext.addError(new ExceptionWhileDataFetching(e)); + stage = completedFuture(null); + } + + return stage.exceptionally(e -> { + log.warn("Exception while fetching data", e); + executionContext.addError(new ExceptionWhileDataFetching(e)); + return null; + }).thenCompose(o -> completeValueAsync(executionContext, fieldDef.getType(), fields, o)); + } + + protected CompletionStage completeValueAsync(ExecutionContext executionContext, GraphQLType fieldType, List fields, Object result) { + if (fieldType instanceof GraphQLNonNull) { + return completeValueAsync(executionContext, ((GraphQLNonNull) fieldType).getWrappedType(), fields, result).thenApply(result1 -> { + if (isNull(result1.getData())) { + throw new NonNullException("Cannot return null for non-nullable type: " + fields); + } + return result1; + }); + } else if (isNull(result)) { + return completedFuture(new ExecutionResultImpl(null, null)); + } else if (fieldType instanceof GraphQLList) { + if (result.getClass().isArray()) { + result = asList((Object[]) result); + } + return completeValueForListAsync(executionContext, (GraphQLList) fieldType, fields, (Iterable) result); + } else if (fieldType instanceof GraphQLScalarType) { + return completedFuture(completeValueForScalar((GraphQLScalarType) fieldType, result)); + } else if (fieldType instanceof GraphQLEnumType) { + return completedFuture(completeValueForEnum((GraphQLEnumType) fieldType, result)); + } + + GraphQLObjectType resolvedType; + if (fieldType instanceof GraphQLInterfaceType) { + resolvedType = resolveType((GraphQLInterfaceType) fieldType, result); + } else if (fieldType instanceof GraphQLUnionType) { + resolvedType = resolveType((GraphQLUnionType) fieldType, result); + } else { + resolvedType = (GraphQLObjectType) fieldType; + } + + Map> subFields = new LinkedHashMap<>(); + List visitedFragments = new ArrayList<>(); + for (Field field : fields) { + if (isNull(field.getSelectionSet())) continue; + fieldCollector.collectFields(executionContext, resolvedType, field.getSelectionSet(), visitedFragments, subFields); + } + + // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. + AsyncExecutionStrategy queryStrategy = (AsyncExecutionStrategy) executionContext.getQueryStrategy(); + return queryStrategy.executeAsync(executionContext, resolvedType, result, subFields); + } + + protected CompletionStage completeValueForListAsync(ExecutionContext executionContext, GraphQLList fieldType, List fields, Iterable result) { + List> completedResults = new ArrayList<>(); + for (Object item : result) { + CompletionStage completedValue = completeValueAsync(executionContext, fieldType.getWrappedType(), fields, item); + completedResults.add(completedValue); + } + return CompletableFutures.allAsList(completedResults).thenApply(results -> { + List items = results.stream().map(ExecutionResult::getData).collect(toList()); + return new ExecutionResultImpl(items, null); + }); + } + + private CompletionStage> executeFields(Map>> map) { + if (serial) { + List>>> resolvers = new ArrayList<>(map.entrySet()); + LinkedHashMap results = new LinkedHashMap<>(); + return executeInSerial(resolvers, results, 0); + } else { + return executeInParallel(map); + } + } + + private CompletionStage> executeInSerial(List>>> resolvers, Map results, int i) { + return resolvers.get(i).getValue().get().thenCompose(result -> { + results.put(resolvers.get(i).getKey(), result); + if (i == resolvers.size() - 1) { + return completedFuture(results); + } else { + return executeInSerial(resolvers, results, i + 1); + } + }); + } + + private CompletionStage> executeInParallel(Map>> resolvers) { + CompletableFuture> future = completableFutureFactory.future(); + Set awaiting = new ConcurrentHashMap<>(resolvers).keySet(); + Map results = new ConcurrentHashMap<>(); + resolvers.entrySet().forEach(entry -> { + entry.getValue().get().thenAccept(result -> { + if (future.isCompletedExceptionally()) { + return; + } + K key = entry.getKey(); + if (!isNull(result)) { + results.put(key, result); + } + awaiting.remove(key); + if (awaiting.isEmpty()) { + Map map = new LinkedHashMap<>(); + resolvers.keySet().forEach(key1 -> map.put(key1, results.get(key1))); + future.complete(map); + } + }).exceptionally(e -> { + future.completeExceptionally(e); + return null; + }); + }); + return future; + } +} diff --git a/src/main/java/graphql/execution/async/CompletableFutureFactory.java b/src/main/java/graphql/execution/async/CompletableFutureFactory.java new file mode 100644 index 0000000000..30d96d5a8d --- /dev/null +++ b/src/main/java/graphql/execution/async/CompletableFutureFactory.java @@ -0,0 +1,7 @@ +package graphql.execution.async; + +import java.util.concurrent.CompletableFuture; + +public interface CompletableFutureFactory { + CompletableFuture future(); +} diff --git a/src/main/java/graphql/execution/async/DefaultCompletableFutureFactory.java b/src/main/java/graphql/execution/async/DefaultCompletableFutureFactory.java new file mode 100644 index 0000000000..44b7f135c5 --- /dev/null +++ b/src/main/java/graphql/execution/async/DefaultCompletableFutureFactory.java @@ -0,0 +1,15 @@ +package graphql.execution.async; + +import java.util.concurrent.CompletableFuture; + +public class DefaultCompletableFutureFactory implements CompletableFutureFactory { + + public static CompletableFutureFactory defaultFactory() { + return new DefaultCompletableFutureFactory(); + } + + @Override + public CompletableFuture future() { + return new CompletableFuture<>(); + } +} diff --git a/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy new file mode 100644 index 0000000000..af240716bc --- /dev/null +++ b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy @@ -0,0 +1,155 @@ +package graphql.execution.async + +import graphql.execution.ExecutionContext +import graphql.execution.ExecutionStrategy +import graphql.language.Field +import graphql.language.SelectionSet +import graphql.schema.* +import spock.lang.Ignore +import spock.lang.Specification +import spock.lang.Unroll + +import static com.spotify.futures.CompletableFutures.exceptionallyCompletedFuture +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLSchema.newSchema +import static java.util.concurrent.CompletableFuture.completedFuture + +class AsyncExecutionStrategyTest extends Specification { + + @Unroll + def "async field"() { + given: + def strategy = AsyncExecutionStrategy.serial() + def parentType = buildParentType(type, fetcher) + def executionContext = buildExecutionContext(strategy, parentType) + def fields = [field: [new Field('field')]] + + when: + def actual = strategy.execute(executionContext, parentType, null, fields); + + then: + actual.data.field == expected + + where: + fetcher | type || expected + { it -> null } | GraphQLString || null + { it -> 'a' } | GraphQLString || 'a' + { it -> 'a' } | new GraphQLNonNull(GraphQLString) || 'a' + { it -> ['a'] } | new GraphQLList(GraphQLString) || ['a'] + { it -> ['a'] } | new GraphQLList(new GraphQLNonNull(GraphQLString)) || ['a'] + { it -> completedFuture(null) } | GraphQLString || null + { it -> completedFuture('value') } | GraphQLString || 'value' + { it -> completedFuture('value') } | new GraphQLNonNull(GraphQLString) || 'value' + { it -> completedFuture(['value']) } | new GraphQLList(new GraphQLNonNull(GraphQLString)) || ['value'] + { it -> throw new RuntimeException() } | GraphQLString || null + { it -> exceptionallyCompletedFuture(new RuntimeException()) } | GraphQLString || null + } + + def "async obj"() { + given: + def type = new GraphQLList(newObject() + .name('Composite') + .field(field('field', GraphQLString, { 'value' })) + .build()) + def strategy = AsyncExecutionStrategy.serial() + def parentType = buildParentType(type, { completedFuture([[field: 'value']]) }) + def executionContext = buildExecutionContext(strategy, parentType) + def fields = [field: [new Field('field', new SelectionSet([new Field('field')]))]] + + when: + def actual = strategy.execute(executionContext, parentType, null, fields); + + then: + actual.data == [field: [[field: 'value']]] + } + + // http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability + // - "Since Non-Null type fields cannot be null, field errors are propagated + // to be handled by the parent field. If the parent field may be null then + // it resolves to null, otherwise if it is a Non-Null type, the field error + // is further propagated to it’s parent field." + // - "...only one error should be added to the errors list per field." + + def "null non-null field results in null nullable parent"() { + given: + def type = newObject() + .name('ParentType') + .field(newFieldDefinition() + .name('field') + .type(new GraphQLNonNull(GraphQLString)) + .dataFetcher({ null })) + .build() + def strategy = AsyncExecutionStrategy.serial() + def executionContext = buildExecutionContext(strategy, type) + def fields = [field: [new Field('field')]] + + when: + def actual = strategy.execute(executionContext, type, [:], fields) + + then: + actual.data == null + } + + def "null non-null fields propagate to nearest nullable parent"() { + given: + def type = newObject() + .name('ParentType') + .field(newFieldDefinition() + .name('nullableField') + .type(newObject() + .name('ChildType') + .field(newFieldDefinition() + .name('nonNullField') + .type(new GraphQLNonNull(newObject() + .name('GrandChildType') + .field(newFieldDefinition() + .name('nonNullField') + .type(new GraphQLNonNull(GraphQLString))).build())))) + .dataFetcher({[nonNullField: [:]]})) + .build() + def strategy = AsyncExecutionStrategy.serial() + def executionContext = buildExecutionContext(strategy, type) + def fields = [nullableField: [new Field('nullableField', new SelectionSet([new Field('nonNullField', new SelectionSet([new Field('nonNullField')]))]))]] + + when: + def actual = strategy.execute(executionContext, type, [:], fields) + + then: + actual.data == [nullableField: null] + } + + @Ignore + def "fields execute in the correct order"() { + + } + + GraphQLFieldDefinition field(String name, GraphQLOutputType type, DataFetcher fetcher) { + newFieldDefinition() + .name(name) + .type(type) + .dataFetcher(fetcher) + .build() + } + + GraphQLObjectType buildParentType(GraphQLOutputType type, DataFetcher fetcher) { + newObject() + .name('object') + .field(field('field', type, fetcher)) + .build() + } + + ExecutionContext buildExecutionContext(ExecutionStrategy strategy, GraphQLObjectType parentType) { + def executionContext = new ExecutionContext( + null, + newSchema().query(parentType).build(), + strategy, + null, + null, + null, + null, + null + ) + } +}