diff --git a/build.gradle b/build.gradle index 662973580e..e892196727 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,3 @@ -import java.text.SimpleDateFormat - plugins { id "com.jfrog.bintray" version "1.2" } @@ -9,10 +7,11 @@ apply plugin: 'maven' apply plugin: 'maven-publish' apply plugin: 'antlr' -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 = "2.3.0-SNAPSHOT" +//version = releaseVersion ? releaseVersion : new SimpleDateFormat('yyyy-MM-dd\'T\'HH-mm-ss').format(new Date()) group = 'com.graphql-java' @@ -29,12 +28,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 93fb1096f2..8cd3588185 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletionStage; import static graphql.Assert.assertNotNull; @@ -25,18 +26,24 @@ public class GraphQL { private final GraphQLSchema graphQLSchema; - private final ExecutionStrategy executionStrategy; + private final ExecutionStrategy queryStrategy; + private final ExecutionStrategy mutationStrategy; private static final Logger log = LoggerFactory.getLogger(GraphQL.class); public GraphQL(GraphQLSchema graphQLSchema) { - this(graphQLSchema, null); + this(graphQLSchema, null, null); } - public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy executionStrategy) { + public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) { + this(graphQLSchema, queryStrategy, null); + } + + public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { this.graphQLSchema = graphQLSchema; - this.executionStrategy = executionStrategy; + this.queryStrategy = queryStrategy; + this.mutationStrategy = mutationStrategy; } public ExecutionResult execute(String requestString) { @@ -74,7 +81,7 @@ public ExecutionResult execute(String requestString, String operationName, Objec if (validationErrors.size() > 0) { return new ExecutionResultImpl(validationErrors); } - Execution execution = new Execution(executionStrategy); + Execution execution = new Execution(queryStrategy, mutationStrategy); return execution.execute(graphQLSchema, context, document, operationName, arguments); } diff --git a/src/main/java/graphql/GraphQLAsync.java b/src/main/java/graphql/GraphQLAsync.java new file mode 100644 index 0000000000..d5deba6a51 --- /dev/null +++ b/src/main/java/graphql/GraphQLAsync.java @@ -0,0 +1,55 @@ +package graphql; + +import graphql.execution.ExecutionStrategy; +import graphql.schema.GraphQLSchema; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +public class GraphQLAsync extends GraphQL { + + public GraphQLAsync(GraphQLSchema graphQLSchema) { + super(graphQLSchema); + } + + public GraphQLAsync(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) { + super(graphQLSchema, queryStrategy); + } + + 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) { + ExecutionResult result = execute(requestString, operationName, context, arguments); + Object data1 = result.getData(); + if (data1 instanceof CompletionStage) { + return ((CompletionStage>) data1).thenApply(data -> new ExecutionResultImpl(data, result.getErrors())); + } + return completedFuture(result); + } +} diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index cf220c06ad..32db165da8 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -17,19 +17,17 @@ public class Execution { private FieldCollector fieldCollector = new FieldCollector(); - private ExecutionStrategy strategy; + private ExecutionStrategy queryStrategy; + private ExecutionStrategy mutationStrategy; - public Execution(ExecutionStrategy strategy) { - this.strategy = strategy; - - if (this.strategy == null) { - this.strategy = new SimpleExecutionStrategy(); - } + public Execution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy) { + this.queryStrategy = queryStrategy != null ? queryStrategy : new SimpleExecutionStrategy(); + this.mutationStrategy = mutationStrategy != null ? mutationStrategy : new SimpleExecutionStrategy(); } public ExecutionResult execute(GraphQLSchema graphQLSchema, Object root, Document document, String operationName, Map args) { ExecutionContextBuilder executionContextBuilder = new ExecutionContextBuilder(new ValuesResolver()); - ExecutionContext executionContext = executionContextBuilder.build(graphQLSchema, strategy, root, document, operationName, args); + ExecutionContext executionContext = executionContextBuilder.build(graphQLSchema, queryStrategy, mutationStrategy, root, document, operationName, args); return executeOperation(executionContext, root, executionContext.getOperationDefinition()); } @@ -55,9 +53,9 @@ private ExecutionResult executeOperation( fieldCollector.collectFields(executionContext, operationRootType, operationDefinition.getSelectionSet(), new ArrayList(), fields); if (operationDefinition.getOperation() == OperationDefinition.Operation.MUTATION) { - return new SimpleExecutionStrategy().execute(executionContext, operationRootType, root, fields); + return mutationStrategy.execute(executionContext, operationRootType, root, fields); } else { - return strategy.execute(executionContext, operationRootType, root, fields); + return queryStrategy.execute(executionContext, operationRootType, root, fields); } } } diff --git a/src/main/java/graphql/execution/ExecutionContext.java b/src/main/java/graphql/execution/ExecutionContext.java index c2f09dd41d..ca4f0f0516 100644 --- a/src/main/java/graphql/execution/ExecutionContext.java +++ b/src/main/java/graphql/execution/ExecutionContext.java @@ -6,20 +6,18 @@ import graphql.language.OperationDefinition; import graphql.schema.GraphQLSchema; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class ExecutionContext { private GraphQLSchema graphQLSchema; - private ExecutionStrategy executionStrategy; + private ExecutionStrategy queryStrategy; + private ExecutionStrategy mutationStrategy; private Map fragmentsByName = new LinkedHashMap(); private OperationDefinition operationDefinition; private Map variables = new LinkedHashMap(); private Object root; - private List errors = new ArrayList(); + private List errors = Collections.synchronizedList(new ArrayList()); public GraphQLSchema getGraphQLSchema() { return graphQLSchema; @@ -73,11 +71,19 @@ public List getErrors() { return errors; } - public ExecutionStrategy getExecutionStrategy() { - return executionStrategy; + public ExecutionStrategy getQueryStrategy() { + return queryStrategy; } - public void setExecutionStrategy(ExecutionStrategy executionStrategy) { - this.executionStrategy = executionStrategy; + public void setQueryStrategy(ExecutionStrategy queryStrategy) { + this.queryStrategy = queryStrategy; + } + + public ExecutionStrategy getMutationStrategy() { + return mutationStrategy; + } + + public void setMutationStrategy(ExecutionStrategy mutationStrategy) { + this.mutationStrategy = mutationStrategy; } } diff --git a/src/main/java/graphql/execution/ExecutionContextBuilder.java b/src/main/java/graphql/execution/ExecutionContextBuilder.java index d16ac1ecbf..cc4e68eacf 100644 --- a/src/main/java/graphql/execution/ExecutionContextBuilder.java +++ b/src/main/java/graphql/execution/ExecutionContextBuilder.java @@ -18,7 +18,7 @@ public ExecutionContextBuilder(ValuesResolver valuesResolver) { this.valuesResolver = valuesResolver; } - public ExecutionContext build(GraphQLSchema graphQLSchema, ExecutionStrategy executionStrategy, Object root, Document document, String operationName, Map args) { + public ExecutionContext build(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, ExecutionStrategy mutationStrategy, Object root, Document document, String operationName, Map args) { Map fragmentsByName = new LinkedHashMap(); Map operationsByName = new LinkedHashMap(); @@ -48,7 +48,8 @@ public ExecutionContext build(GraphQLSchema graphQLSchema, ExecutionStrategy exe ExecutionContext executionContext = new ExecutionContext(); executionContext.setGraphQLSchema(graphQLSchema); - executionContext.setExecutionStrategy(executionStrategy); + executionContext.setQueryStrategy(queryStrategy); + executionContext.setMutationStrategy(mutationStrategy); executionContext.setOperationDefinition(operation); executionContext.setRoot(root); executionContext.setFragmentsByName(fragmentsByName); diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index 68efbf22ee..2747cecdf5 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -82,10 +82,9 @@ protected ExecutionResult completeValue(ExecutionContext executionContext, Graph fieldCollector.collectFields(executionContext, resolvedType, field.getSelectionSet(), visitedFragments, subFields); } - // Calling this from the executionContext so that you can shift from the simple execution strategy for mutations - // back to the desired strategy. + // Calling this from the executionContext to ensure we shift back from mutation strategy to the query strategy. - return executionContext.getExecutionStrategy().execute(executionContext, resolvedType, result, subFields); + return executionContext.getQueryStrategy().execute(executionContext, resolvedType, result, subFields); } private ExecutionResult completeValueForList(ExecutionContext executionContext, GraphQLList fieldType, List fields, Object result) { 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..1402d4baf9 --- /dev/null +++ b/src/main/java/graphql/execution/async/AsyncExecutionStrategy.java @@ -0,0 +1,131 @@ +package graphql.execution.async; + +import graphql.ExceptionWhileDataFetching; +import graphql.ExecutionResult; +import graphql.GraphQLError; +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.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + +import static com.spotify.futures.CompletableFutures.successfulAsList; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + + +public final class AsyncExecutionStrategy extends ExecutionStrategy { + + public static AsyncExecutionStrategy serial() { + return new AsyncExecutionStrategy(true); + } + + public static AsyncExecutionStrategy parallel() { + return new AsyncExecutionStrategy(false); + } + + private static final Logger log = LoggerFactory.getLogger(AsyncExecutionStrategy.class); + + private final boolean serial; + + private AsyncExecutionStrategy(boolean serial) { + this.serial = serial; + } + + @Override + public ExecutionResult execute(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, Map> fields) { + Map>> fieldResolvers = fields.entrySet() + .stream() + .collect(toMap(Map.Entry::getKey, entry -> () -> resolveFieldAsync(executionContext, parentType, source, entry.getValue()))); + + AsyncFieldsCoordinator coordinator = new AsyncFieldsCoordinator(fieldResolvers); + + CompletionStage> data = serial ? coordinator.executeSerially() : coordinator.executeParallelly(); + return new ExecutionResultImpl(data, executionContext.getErrors()); + } + + private CompletionStage resolveFieldAsync(ExecutionContext executionContext, GraphQLObjectType parentType, Object source, List fieldList) { + GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, fieldList.get(0)); + GraphQLOutputType fieldType = fieldDef.getType(); + + DataFetchingEnvironment env = new DataFetchingEnvironment( + source, + valuesResolver.getArgumentValues(fieldDef.getArguments(), fieldList.get(0).getArguments(), executionContext.getVariables()), + executionContext.getRoot(), + fieldList, + fieldDef.getType(), + parentType, + executionContext.getGraphQLSchema() + ); + + try { + Object obj1 = fieldDef.getDataFetcher().get(env); + if (obj1 instanceof CompletionStage) { + return ((CompletionStage) obj1) + .exceptionally(e -> { + logExceptionWhileFetching(e, fieldList.get(0)); + executionContext.addError(new ExceptionWhileDataFetching(e)); + return null; + }) + .thenCompose(obj2 -> completeValueAsync(executionContext, fieldType, fieldList, obj2)); + } else { + return completeValueAsync(executionContext, fieldType, fieldList, obj1); + } + } catch (Exception e) { + logExceptionWhileFetching(e, fieldList.get(0)); + executionContext.addError(new ExceptionWhileDataFetching(e)); + return completedFuture(new ExecutionResultImpl(null, null)); + } + } + + @SuppressWarnings("unchecked") + private CompletionStage completeValueAsync(ExecutionContext executionContext, GraphQLType fieldType, List fieldList, Object result) { + if (fieldType instanceof GraphQLList) { + List collect = ((List) result); + + List> collect1 = collect.stream() + .map(object -> { + CompletionStage stage = object instanceof CompletionStage ? (CompletionStage) object : completedFuture(object); + return stage + .thenCompose(result5 -> completeValueAsync(executionContext, ((GraphQLList) fieldType).getWrappedType(), fieldList, result5)); + }) + .collect(toList()); + + return successfulAsList(collect1, t -> null).thenApply(results -> { + List list = new ArrayList<>(); + List errors = new ArrayList<>(); + for (ExecutionResult executionResult : results) { + list.add(executionResult.getData()); + errors.addAll(executionResult.getErrors()); + } + return new ExecutionResultImpl(list, errors); + }); + + } else { + ExecutionResult completed = completeValue(executionContext, fieldType, fieldList, result); + // Happens when the data fetcher returns null for nullable field + if (completed == null) { + return completedFuture(new ExecutionResultImpl(null, null)); + } + if (!(completed.getData() instanceof CompletionStage)) { + return completedFuture(completed); + } + return ((CompletionStage) completed.getData()) + .thenApply(data -> new ExecutionResultImpl(data, completed.getErrors())); + } + } + + private void logExceptionWhileFetching(Throwable e, Field field) { + log.debug("Exception while fetching data for field {}", field.getName(), e); + } + +} diff --git a/src/main/java/graphql/execution/async/AsyncFieldsCoordinator.java b/src/main/java/graphql/execution/async/AsyncFieldsCoordinator.java new file mode 100644 index 0000000000..b3454276d9 --- /dev/null +++ b/src/main/java/graphql/execution/async/AsyncFieldsCoordinator.java @@ -0,0 +1,65 @@ +package graphql.execution.async; + +import graphql.ExecutionResult; +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.Supplier; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +class AsyncFieldsCoordinator { + + private static final Logger log = LoggerFactory.getLogger(AsyncFieldsCoordinator.class); + + private final Map>> resolvers; + + AsyncFieldsCoordinator(Map>> resolvers) { + this.resolvers = resolvers; + } + + public CompletionStage> executeSerially() { + return executeSerially(new ArrayList<>(resolvers.entrySet()), new LinkedHashMap<>(), 0); + } + + private CompletionStage> executeSerially(List>>> resolvers, Map results, int i) { + return resolvers.get(i).getValue().get().thenCompose(result -> { + results.put(resolvers.get(i).getKey(), result.getData()); + if (i == resolvers.size() - 1) { + return completedFuture(results); + } else { + return executeSerially(resolvers, results, i + 1); + } + }); + } + + private static final Object NULL = new Object(); + + public CompletionStage> executeParallelly() { + CompletableFuture> future = new CompletableFuture<>(); + Set awaiting = new ConcurrentHashMap<>(new HashMap<>(resolvers)).keySet(); // `keySet()` is a view and will be modified, so copy first + Map results = new ConcurrentHashMap<>(); + resolvers.entrySet().forEach(entry -> { + entry.getValue().get().thenAccept(result -> { + String key = entry.getKey(); + Object value1 = result.getData() != null ? result.getData() : NULL; + results.put(key, value1); + awaiting.remove(key); + if (awaiting.isEmpty()) { + Map map = new LinkedHashMap<>(); + resolvers.keySet().forEach(fieldName -> { + Object value = results.get(fieldName); + map.put(fieldName, value == NULL ? null : value); + }); + log.trace("completing"); + future.complete(map); + } + }); + }); + return future; + } +} diff --git a/src/main/java/graphql/execution/async/ExecutionResultImpl.java b/src/main/java/graphql/execution/async/ExecutionResultImpl.java new file mode 100644 index 0000000000..41bc261ca7 --- /dev/null +++ b/src/main/java/graphql/execution/async/ExecutionResultImpl.java @@ -0,0 +1,32 @@ +package graphql.execution.async; + +import graphql.ExecutionResult; +import graphql.GraphQLError; + +import java.util.List; + +/** + * `graphql.ExecutionResultImpl` shallow copies the list of passed-in errors before storing it, + * which prevents sharing the list of errors among threads. This implementation stores and encapsulates + * the errors list reference directly so that it may be shared among threads. + */ +class ExecutionResultImpl implements ExecutionResult { + + private final Object data; + private final List errors; + + ExecutionResultImpl(Object data, List errors) { + this.data = data; + this.errors = errors; + } + + @Override + public Object getData() { + return data; + } + + @Override + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/graphql/validation/rules/KnownArgumentNames.java b/src/main/java/graphql/validation/rules/KnownArgumentNames.java index 4186623a92..8f8f26bf54 100644 --- a/src/main/java/graphql/validation/rules/KnownArgumentNames.java +++ b/src/main/java/graphql/validation/rules/KnownArgumentNames.java @@ -2,6 +2,7 @@ import graphql.language.Argument; import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLFieldDefinition; import graphql.validation.*; @@ -15,12 +16,23 @@ public KnownArgumentNames(ValidationContext validationContext, ValidationErrorCo @Override public void checkArgument(Argument argument) { + GraphQLDirective directive = getValidationContext().getDirective(); + if (directive != null) { + GraphQLArgument directiveArgument = directive.getArgument(argument.getName()); + if (directiveArgument == null) { + String message = String.format("Unknown argument %s on directive %s", argument.getName(), directive.getName()); + addError(new ValidationError(ValidationErrorType.UnknownArgument, argument.getSourceLocation(), message)); + } + return; + } GraphQLFieldDefinition fieldDef = getValidationContext().getFieldDef(); - if (fieldDef == null) return; - GraphQLArgument fieldArgument = fieldDef.getArgument(argument.getName()); - if (fieldArgument == null) { - String message = String.format("Unknown argument %s", argument.getName()); - addError(new ValidationError(ValidationErrorType.UnknownArgument, argument.getSourceLocation(), message)); + if (fieldDef != null) { + GraphQLArgument fieldArgument = fieldDef.getArgument(argument.getName()); + if (fieldArgument == null) { + String message = String.format("Unknown argument %s onf field %s", argument.getName(), fieldDef.getName()); + addError(new ValidationError(ValidationErrorType.UnknownArgument, argument.getSourceLocation(), message)); + } + } } } 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..5ff8fb9eaa --- /dev/null +++ b/src/test/groovy/graphql/execution/async/AsyncExecutionStrategyTest.groovy @@ -0,0 +1,122 @@ +package graphql.execution.async + +import graphql.ExceptionWhileDataFetching +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.util.concurrent.AsyncConditions + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionStage + +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 { + + def exception = new RuntimeException(); + + def executionContext + + def "produces the correct results"() { + given: + def strategy = AsyncExecutionStrategy.serial() + + def composite = newObject() + .name('composite') + .field(field('field', GraphQLString, { env -> + env.getSource()['field'] + })) + .build() + + def parentType = newObject() + .name('object') + .field(field('field', GraphQLString, { completedFuture('string') })) + .field(field('listOfScalars', new GraphQLList(GraphQLString), { completedFuture(['a']) })) + .field(field('listOfScalars2', new GraphQLList(GraphQLString), { [completedFuture('a')] })) + .field(field('listOfComposite', new GraphQLList(composite), { [[field: 'value']] })) + .field(field('listOfFutureComposite', new GraphQLList(composite), { [completedFuture([field: 'value'])] })) + .field(field('completesExceptionally', GraphQLString, { exceptionallyCompletedFuture(exception) })) + .field(field('throwsException', GraphQLString, { throw exception })) + .build() + + executionContext = buildExecutionContext(strategy, parentType) + +// def fields = [ +// listOfFutureComposite: [new Field('listOfFutureComposite', new SelectionSet([new Field('field')]))], +// ] + + def fields = [ + listOfScalars2 : [new Field('listOfScalars2')], + field : [new Field('field')], + listOfScalars : [new Field('listOfScalars')], + listOfComposite : [new Field('listOfComposite', new SelectionSet([new Field('field')]))], + listOfFutureComposite : [new Field('listOfFutureComposite', new SelectionSet([new Field('field')]))], + completesExceptionally: [new Field('completesExceptionally')], + throwsException : [new Field('throwsException')] + ]; + + when: + def result = strategy.execute(executionContext, parentType, null, fields) + + then: + def conds = new AsyncConditions(1) + + result.getData().thenAccept({ data -> + conds.evaluate { +// assert data == [ +// listOfFutureComposite: [[field: 'value']], +// ] + assert data == [ + listOfScalars2 : ['a'], + field : 'string', + listOfScalars : ['a'], + listOfComposite : [[field: 'value']], + listOfFutureComposite : [[field: 'value']], + completesExceptionally: null, + throwsException : null + ] + assert result.errors.size() == 2 + (0..1).each { + result.errors[it] instanceof ExceptionWhileDataFetching + result.errors[it].exception == exception + } + } + }) + + conds.await() + } + + @Ignore + def "in the correct order"() { + + } + + public CompletionStage exceptionallyCompletedFuture(Throwable exception) { + def future = new CompletableFuture<>(); + future.completeExceptionally(exception); + future; + } + + GraphQLFieldDefinition field(String name, GraphQLOutputType type, DataFetcher fetcher) { + newFieldDefinition() + .name(name) + .type(type) + .dataFetcher(fetcher) + .build() + } + + ExecutionContext buildExecutionContext(ExecutionStrategy strategy, GraphQLObjectType parentType) { + executionContext = new ExecutionContext() + executionContext.setGraphQLSchema(newSchema().query(parentType).build()) + executionContext.setQueryStrategy(strategy) + executionContext + } +} diff --git a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy index 5c6a7aa4c5..b6cf9349b7 100644 --- a/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy +++ b/src/test/groovy/graphql/validation/rules/KnownArgumentNamesTest.groovy @@ -1,6 +1,8 @@ package graphql.validation.rules +import graphql.Directives import graphql.language.Argument +import graphql.language.BooleanValue import graphql.language.StringValue import graphql.schema.GraphQLFieldDefinition import graphql.validation.ValidationContext @@ -9,6 +11,7 @@ import graphql.validation.ValidationErrorType import spock.lang.Specification import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLArgument.newArgument class KnownArgumentNamesTest extends Specification { @@ -16,14 +19,29 @@ class KnownArgumentNamesTest extends Specification { ValidationErrorCollector errorCollector = new ValidationErrorCollector() KnownArgumentNames knownArgumentNames = new KnownArgumentNames(validationContext, errorCollector) + def "unknown argument"(){ given: - Argument argument = new Argument("unknownArg",new StringValue("value")) def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").type(GraphQLString).build(); + Argument argument = new Argument("unknownArg",new StringValue("value")) validationContext.getFieldDef() >> fieldDefinition when: knownArgumentNames.checkArgument(argument) then: errorCollector.containsValidationError(ValidationErrorType.UnknownArgument) } + + def "directives"() { + given: + def fieldArg = newArgument().name("arg").type(GraphQLString) + def fieldDefinition = GraphQLFieldDefinition.newFieldDefinition().name("field").argument(fieldArg).type(GraphQLString).build(); + def directive = Directives.IncludeDirective + validationContext.getFieldDef() >> fieldDefinition + validationContext.getDirective() >> directive + def argument = new Argument("if", new BooleanValue(true)) + when: + knownArgumentNames.checkArgument(argument) + then: + !errorCollector.containsValidationError(ValidationErrorType.UnknownArgument) + } }