From 98566789b95f70758ccfc79f5b9840665d288c8c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 5 Dec 2023 16:09:08 +1100 Subject: [PATCH 01/33] WIP: Implement support for defer in ExecutableNormalizedOperationFactory --- src/main/java/graphql/Directives.java | 57 +++ .../normalized/ExecutableNormalizedField.java | 19 + .../ExecutableNormalizedOperationFactory.java | 72 +++- ...tableNormalizedOperationToAstCompiler.java | 2 +- .../incremental/DeferExecution.java | 40 ++ .../normalized/incremental/DeferLabel.java | 34 ++ .../incremental/IncrementalNodes.java | 51 +++ ...NormalizedOperationFactoryDeferTest.groovy | 399 ++++++++++++++++++ ...izedOperationToAstCompilerDeferTest.groovy | 114 +++++ 9 files changed, 766 insertions(+), 22 deletions(-) create mode 100644 src/main/java/graphql/normalized/incremental/DeferExecution.java create mode 100644 src/main/java/graphql/normalized/incremental/DeferLabel.java create mode 100644 src/main/java/graphql/normalized/incremental/IncrementalNodes.java create mode 100644 src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy create mode 100644 src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 50e570e14a..61358c5b68 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -1,6 +1,7 @@ package graphql; +import graphql.language.BooleanValue; import graphql.language.Description; import graphql.language.DirectiveDefinition; import graphql.language.StringValue; @@ -33,6 +34,7 @@ public class Directives { private static final String SPECIFIED_BY = "specifiedBy"; private static final String DEPRECATED = "deprecated"; private static final String ONE_OF = "oneOf"; + private static final String DEFER = "defer"; public static final String NO_LONGER_SUPPORTED = "No longer supported"; public static final DirectiveDefinition DEPRECATED_DIRECTIVE_DEFINITION; @@ -40,6 +42,13 @@ public class Directives { @ExperimentalApi public static final DirectiveDefinition ONE_OF_DIRECTIVE_DEFINITION; + /** + * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in + * directive that is not available unless it is explicitly put into the schema. + */ +// @ExperimentalApi +// public static final DirectiveDefinition DEFER_DIRECTIVE_DEFINITION; + static { DEPRECATED_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() @@ -75,8 +84,56 @@ public class Directives { .directiveLocation(newDirectiveLocation().name(INPUT_OBJECT.name()).build()) .description(createDescription("Indicates an Input Object is a OneOf Input Object.")) .build(); + +// DEFER_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() +// .name(DEFER) +// .description(createDescription("This directive allows results to be deferred during execution")) +// .directiveLocation(newDirectiveLocation().name(FRAGMENT_SPREAD.name()).build()) +// .directiveLocation(newDirectiveLocation().name(INLINE_FRAGMENT.name()).build()) +// .inputValueDefinition( +// newInputValueDefinition() +// .name("if") +// .description(createDescription("Deferred behaviour is controlled by this argument")) +// .type(newTypeName().name("Boolean").build()) +// .defaultValue(BooleanValue.newBooleanValue(true).build()) +// .build()) +// .inputValueDefinition( +// newInputValueDefinition() +// // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: +// // > `label` must not be provided as a variable. +// // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query +// .name("label") +// .description(createDescription("A unique label that represents the fragment being deferred")) +// .type(newTypeName().name("Boolean").build()) +// .defaultValue(BooleanValue.newBooleanValue(true).build()) +// .build()) +// .build(); } + /** + * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in + * directive that is not available unless it is explicitly put into the schema. + */ + public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() + .name("defer") + .description("This directive allows results to be deferred during execution") + .validLocations(FRAGMENT_SPREAD, INLINE_FRAGMENT) + .argument(newArgument() + .name("if") + .type(nonNull(GraphQLBoolean)) + .description("Deferred behaviour is controlled by this argument") + .defaultValueLiteral(BooleanValue.newBooleanValue(true).build()) + ) + .argument(newArgument() + // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: + // > `label` must not be provided as a variable. + // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query + .name("label") + .type(GraphQLString) + .description("A unique label that represents the fragment being deferred") + ) + .build(); + public static final GraphQLDirective IncludeDirective = GraphQLDirective.newDirective() .name("include") .description("Directs the executor to include this field or fragment only when the `if` argument is true") diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 41ddd594b3..73c1df4d0b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -9,6 +9,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -63,6 +64,7 @@ public class ExecutableNormalizedField { private final String fieldName; private final int level; + private final DeferExecution deferExecution; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -74,6 +76,7 @@ private ExecutableNormalizedField(Builder builder) { this.children = builder.children; this.level = builder.level; this.parent = builder.parent; + this.deferExecution = builder.deferExecution; } /** @@ -365,6 +368,14 @@ public String getSingleObjectTypeName() { } + /** + * TODO Javadoc + * @return + */ + public DeferExecution getDeferExecution() { + return deferExecution; + } + /** * @return a helper method show field details */ @@ -588,6 +599,8 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); + private DeferExecution deferExecution; + private Builder() { } @@ -596,6 +609,7 @@ private Builder(ExecutableNormalizedField existing) { this.normalizedArguments = existing.normalizedArguments; this.astArguments = existing.astArguments; this.resolvedArguments = existing.resolvedArguments; + this.deferExecution = existing.deferExecution; this.objectTypeNames = new LinkedHashSet<>(existing.getObjectTypeNames()); this.fieldName = existing.getFieldName(); this.children = new ArrayList<>(existing.children); @@ -656,6 +670,11 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } + public Builder deferExecution(DeferExecution deferExecution) { + this.deferExecution = deferExecution; + return this; + } + public ExecutableNormalizedField build() { return new ExecutableNormalizedField(this); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 1124c9b7ce..1af327ae39 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -26,6 +26,9 @@ import graphql.language.Selection; import graphql.language.SelectionSet; import graphql.language.VariableDefinition; +import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.DeferLabel; +import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -146,6 +149,7 @@ public int getMaxChildrenDepth() { } private final ConditionalNodes conditionalNodes = new ConditionalNodes(); + private final IncrementalNodes incrementalNodes = new IncrementalNodes(); /** * This will create a runtime representation of the graphql operation that would be executed @@ -155,7 +159,6 @@ public int getMaxChildrenDepth() { * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param coercedVariableValues the coerced variables to use - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( @@ -181,7 +184,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param operationDefinition the operation to be executed * @param fragments a set of fragments associated with the operation * @param coercedVariableValues the coerced variables to use - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation(GraphQLSchema graphQLSchema, @@ -204,7 +206,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param rawVariables the raw variables to be coerced - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, @@ -229,7 +230,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param rawVariables the raw variables that have not yet been coerced * @param locale the {@link Locale} to use during coercion * @param graphQLContext the {@link GraphQLContext} to use during coercion - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables( @@ -258,7 +258,6 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param operationName the operation name to use * @param rawVariables the raw variables that have not yet been coerced * @param options the {@link Options} to use for parsing - * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, @@ -465,7 +464,8 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam fieldAndAstParent.field.getSelectionSet(), collectedFields, (GraphQLCompositeType) astParentType, - possibleObjects + possibleObjects, + null ); } Map> fieldsByName = fieldsByResultKey(collectedFields); @@ -492,7 +492,7 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Set possibleObjects = ImmutableSet.of(rootType); List collectedFields = new ArrayList<>(); - collectFromSelectionSet(parameters, operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects); + collectFromSelectionSet(parameters, operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects, null); // group by result key Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); @@ -554,33 +554,50 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p .fieldName(fieldName) .level(level) .parent(parent) + .deferExecution(collectedFieldGroup.deferExecution) .build(); } private static class CollectedFieldGroup { Set objectTypes; Set fields; + DeferExecution deferExecution; - public CollectedFieldGroup(Set fields, Set objectTypes) { + public CollectedFieldGroup(Set fields, Set objectTypes, DeferExecution deferExecution) { this.fields = fields; this.objectTypes = objectTypes; + this.deferExecution = deferExecution; } } private List groupByCommonParents(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); + DeferExecution deferExecution = null; for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); + + DeferLabel collectedDeferLabel = collectedField.deferLabel; + + if (collectedDeferLabel == null) { + continue; + } + + if (deferExecution == null) { + deferExecution = new DeferExecution(); + } + + deferExecution.addLabel(collectedDeferLabel); } + Set allRelevantObjects = objectTypes.build(); Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects)); + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecution)); } ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType))); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecution)); } return result.build(); } @@ -590,15 +607,16 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet SelectionSet selectionSet, List result, GraphQLCompositeType astTypeCondition, - Set possibleObjects + Set possibleObjects, + DeferLabel deferLabel ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { - collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition); + collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition, deferLabel); } else if (selection instanceof InlineFragment) { - collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition); + collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition, deferLabel); } else if (selection instanceof FragmentSpread) { - collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects); + collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects, deferLabel); } } } @@ -608,10 +626,13 @@ private static class CollectedField { Set objectTypes; GraphQLCompositeType astTypeCondition; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition) { + DeferLabel deferLabel; + + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferLabel deferLabel) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; + this.deferLabel = deferLabel; } public boolean isAbstract() { @@ -626,7 +647,8 @@ public boolean isConcrete() { private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameters, List result, FragmentSpread fragmentSpread, - Set possibleObjects + Set possibleObjects, + DeferLabel deferLabel ) { if (!conditionalNodes.shouldInclude(fragmentSpread, parameters.getCoercedVariableValues(), @@ -642,9 +664,12 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter parameters.getGraphQLContext())) { return; } + + DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); + GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); + collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); } @@ -652,7 +677,8 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter List result, InlineFragment inlineFragment, Set possibleObjects, - GraphQLCompositeType astTypeCondition + GraphQLCompositeType astTypeCondition, + DeferLabel deferLabel ) { if (!conditionalNodes.shouldInclude(inlineFragment, parameters.getCoercedVariableValues(), parameters.getGraphQLSchema(), parameters.getGraphQLContext())) { return; @@ -665,14 +691,18 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); } - collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects); + + DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); + + collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); } private void collectField(FieldCollectorNormalizedQueryParams parameters, List result, Field field, Set possibleObjectTypes, - GraphQLCompositeType astTypeCondition + GraphQLCompositeType astTypeCondition, + DeferLabel deferLabel ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), @@ -684,7 +714,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, if (possibleObjectTypes.isEmpty()) { return; } - result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition)); + result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferLabel)); } private Set narrowDownPossibleObjects(Set currentOnes, diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 7dc3d11f32..9d323f1914 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -46,7 +46,7 @@ /** * This class can take a list of {@link ExecutableNormalizedField}s and compiling out a * normalised operation {@link Document} that would represent how those fields - * maybe executed. + * may be executed. *

* This is essentially the reverse of {@link ExecutableNormalizedOperationFactory} which takes * operation text and makes {@link ExecutableNormalizedField}s from it, this takes {@link ExecutableNormalizedField}s diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java new file mode 100644 index 0000000000..892a253238 --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -0,0 +1,40 @@ +package graphql.normalized.incremental; + +import graphql.ExperimentalApi; + +import java.util.HashSet; +import java.util.Set; + +/** + * This class holds information about the defer execution of an ENF. + *

+ * Given that an ENF can be linked with numerous defer labels, a {@link DeferExecution} instance comprises a + * collection of these labels. + *

+ * For example, this query: + *

+ *   query q {
+ *    dog {
+ *      ... @defer(label: "name-defer") {
+ *        name
+ *      }
+ *      ... @defer(label: "another-name-defer") {
+ *        name
+ *      }
+ *    }
+ *  }
+ *  
+ * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" + */ +@ExperimentalApi +public class DeferExecution { + private final Set labels = new HashSet<>(); + + public void addLabel(DeferLabel label) { + this.labels.add(label); + } + + public Set getLabels() { + return this.labels; + } +} diff --git a/src/main/java/graphql/normalized/incremental/DeferLabel.java b/src/main/java/graphql/normalized/incremental/DeferLabel.java new file mode 100644 index 0000000000..d965e5636d --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/DeferLabel.java @@ -0,0 +1,34 @@ +package graphql.normalized.incremental; + +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Holds the value of the 'label' argument of a defer directive. + */ +public class DeferLabel { + private final String value; + + public DeferLabel(@Nullable String value) { + this.value = value; + } + + @Nullable + public String getValue() { + return value; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.value); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof DeferLabel) { + return Objects.equals(this.value, ((DeferLabel) obj).value); + } + + return false; + } +} diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java new file mode 100644 index 0000000000..4763c6c9d2 --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -0,0 +1,51 @@ +package graphql.normalized.incremental; + +import graphql.Assert; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.CoercedVariables; +import graphql.execution.ValuesResolver; +import graphql.language.Directive; +import graphql.language.NodeUtil; +import graphql.schema.GraphQLSchema; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static graphql.Directives.DeferDirective; + +@Internal +public class IncrementalNodes { + + public DeferLabel getDeferLabel( + Map variables, + List directives + ) { + Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); + + if (deferDirective != null) { + Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); + + Object flag = argumentValues.get("if"); + Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + if(!((Boolean) flag)) { + return null; + } + + Object label = argumentValues.get("label"); + + if(label == null) { + return new DeferLabel(null); + } + + Assert.assertTrue(label instanceof String, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + return new DeferLabel((String) label); + + } + + return null; + } +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy new file mode 100644 index 0000000000..110a3353f0 --- /dev/null +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -0,0 +1,399 @@ +package graphql.normalized + +import graphql.AssertException +import graphql.ExecutionInput +import graphql.GraphQL +import graphql.TestUtil +import graphql.execution.RawVariables +import graphql.language.Document +import graphql.schema.GraphQLSchema +import graphql.util.TraversalControl +import graphql.util.Traverser +import graphql.util.TraverserContext +import graphql.util.TraverserVisitorStub +import spock.lang.Specification + +class ExecutableNormalizedOperationFactoryDeferTest extends Specification { + String schema = """ + directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD + + type Query { + dog: Dog + } + + type Dog { + name: String + breed: String + owner: Person + friends: [Dog] + } + + type Person { + firstname: String + lastname: String + friends: [Person] + } + """ + + GraphQLSchema graphQLSchema = TestUtil.schema(schema) + + def "defer on a single field via inline fragment without type"() { + given: + + String query = ''' + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name', + 'Dog.breed defer[breed-defer]', + ] + } + + def "defer on a single field via inline fragment with type"() { + given: + + String query = ''' + query q { + dog { + name + ... on Dog @defer(label: "breed-defer") { + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name', + 'Dog.breed defer[breed-defer]', + ] + } + + def "defer on 2 fields"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "breed-defer") { + name + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[breed-defer]', + 'Dog.breed defer[breed-defer]', + ] + } + + def "defer on a fragment definition"() { + given: + + String query = ''' + query q { + dog { + ... DogFrag @defer(label: "breed-defer") + } + } + + fragment DogFrag on Dog { + name + breed + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[breed-defer]', + 'Dog.breed defer[breed-defer]', + ] + } + + def "multiple defer on same field with different labels"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "name-defer") { + name + } + + ... @defer(label: "another-name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer,another-name-defer]', + ] + } + + def "multiple fields and a single defer"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "name-defer") { + name + } + + ... { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer]', + ] + } + + def "multiple fields and a single defer - no label"() { + given: + + String query = ''' + query q { + dog { + ... @defer { + name + } + + ... { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[null]', + ] + } + + def "multiple fields and a multiple defers with same label"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label:"name-defer") { + name + } + + ... @defer(label:"name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer]', + ] + } + + def "nested defers - no label"() { + given: + + String query = ''' + query q { + dog { + ... @defer { + name + owner { + firstname + ... @defer { + lastname + } + } + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[null]', + 'Dog.owner defer[null]', + 'Person.firstname', + 'Person.lastname defer[null]', + ] + } + + def "nested defers - with labels"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label:"dog-defer") { + name + owner { + firstname + ... @defer(label: "lastname-defer") { + lastname + } + } + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[dog-defer]', + 'Dog.owner defer[dog-defer]', + 'Person.firstname', + 'Person.lastname defer[lastname-defer]', + ] + } + + def "nesting defer blocks that would always result in no data are ignored"() { + given: + + String query = ''' + query q { + dog { + ... @defer(label: "one") { + ... @defer(label: "two") { + ... @defer(label: "three") { + name + } + } + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[three]', + ] + } + + private List executeQueryAndPrintTree(String query, Map variables) { + assertValidQuery(graphQLSchema, query, variables) + Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + return printTreeWithIncrementalExecutionDetails(tree) + } + + private List printTreeWithIncrementalExecutionDetails(ExecutableNormalizedOperation queryExecutionTree) { + def result = [] + Traverser traverser = Traverser.depthFirst({ it.getChildren() }) + traverser.traverse(queryExecutionTree.getTopLevelFields(), new TraverserVisitorStub() { + @Override + TraversalControl enter(TraverserContext context) { + ExecutableNormalizedField queryExecutionField = context.thisNode() + result << queryExecutionField.printDetails() + printDeferExecutionDetails(queryExecutionField) + return TraversalControl.CONTINUE + } + + String printDeferExecutionDetails(ExecutableNormalizedField field) { + if (field.getDeferExecution() == null) { + return "" + } + + def deferLabels = field.getDeferExecution().labels + .collect {it.value} + .join(",") + + return " defer[" + deferLabels + "]" + } + }) + + result + } + + private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() + def ei = ExecutionInput.newExecutionInput(query).variables(variables).build() + assert graphQL.execute(ei).errors.size() == 0 + } +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy new file mode 100644 index 0000000000..2400fa8ca0 --- /dev/null +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -0,0 +1,114 @@ +package graphql.normalized + +import graphql.GraphQL +import graphql.TestUtil +import graphql.execution.RawVariables +import graphql.language.AstPrinter +import graphql.language.AstSorter +import graphql.language.Document +import graphql.schema.GraphQLSchema +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.TestLiveMockedWiringFactory +import graphql.schema.scalars.JsonScalar +import spock.lang.Specification + +import static graphql.ExecutionInput.newExecutionInput +import static graphql.language.OperationDefinition.Operation.QUERY +import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocument + +class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification { + VariablePredicate noVariables = new VariablePredicate() { + @Override + boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { + return false + } + } + + VariablePredicate jsonVariables = new VariablePredicate() { + @Override + boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { + "JSON" == normalizedInputValue.unwrappedTypeName && normalizedInputValue.value != null + } + } + + VariablePredicate allVariables = new VariablePredicate() { + @Override + boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { + return true + } + } + + String sdl = """ + directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD + + type Query { + dog: Dog + } + + type Dog { + name: String + breed: String + owner: Person + friends: [Dog] + } + + type Person { + firstname: String + lastname: String + friends: [Person] + } + """ + + def "simple defer"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } +''' + } + + private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { + assertValidQuery(schema, query, variables) + Document originalDocument = TestUtil.parseQuery(query) + + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) + } + + private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { + return createNormalizedTree(schema, query, variables).getTopLevelFields() + } + + private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { + GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() + assert graphQL.execute(newExecutionInput().query(query).variables(variables)).errors.isEmpty() + } + + GraphQLSchema mkSchema(String sdl) { + def wiringFactory = new TestLiveMockedWiringFactory([JsonScalar.JSON_SCALAR]) + def runtimeWiring = RuntimeWiring.newRuntimeWiring() + .wiringFactory(wiringFactory).build() + TestUtil.schema(sdl, runtimeWiring) + } +} From 37b1c4a671dd79b2b9684efcb8aa4c06a13957ba Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 5 Dec 2023 17:55:18 +1100 Subject: [PATCH 02/33] WIP: ENF to AST Compiler basic implementation --- ...tableNormalizedOperationToAstCompiler.java | 110 ++++++++--- .../incremental/DeferExecution.java | 6 +- ...NormalizedOperationFactoryDeferTest.groovy | 43 +++- ...izedOperationToAstCompilerDeferTest.groovy | 185 +++++++++++++++++- 4 files changed, 301 insertions(+), 43 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 9d323f1914..74250e80ae 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -18,8 +18,10 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; +import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; +import graphql.normalized.incremental.DeferLabel; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -32,6 +34,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import static graphql.collect.ImmutableKit.emptyList; @@ -82,7 +85,7 @@ public Map getVariables() { /** * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s - * + *

* The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * @@ -91,7 +94,6 @@ public Map getVariables() { * @param operationName the name of the operation to use * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -99,22 +101,21 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @Nullable String operationName, @NotNull List topLevelFields, @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema,operationKind,operationName,topLevelFields,Map.of(),variablePredicate); + return compileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); } /** * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s - * + *

* The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * - * @param schema the graphql schema to use - * @param operationKind the kind of operation - * @param operationName the name of the operation to use - * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from - * @param normalizedFieldToQueryDirectives the map of normalized field to query directives - * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param normalizedFieldToQueryDirectives the map of normalized field to query directives + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -153,27 +154,61 @@ private static List> subselectionsForNormalizedField(GraphQLSchema // All conditional fields go here instead of directly to selections, so they can be grouped together // in the same inline fragment in the output - Map> fieldsByTypeCondition = new LinkedHashMap<>(); + // + Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) + .forEach((objectTypeName, field) -> { + if (nf.getDeferExecution() == null) { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) + .add(field); + } else { + nf.getDeferExecution().getLabels().stream() + .map(DeferLabel::getValue) + .forEach(label -> fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, new DeferLabel(label)), ignored -> new ArrayList<>()) + .add(field)); + } + }); + + } else if (nf.getDeferExecution() != null) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) .forEach((objectTypeName, field) -> - fieldsByTypeCondition - .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) - .add(field)); + nf.getDeferExecution().getLabels().stream() + .map(DeferLabel::getValue) + .forEach(label -> fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) + .add(field)) + ); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives,variableAccumulator)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator)); } } - fieldsByTypeCondition.forEach((objectTypeName, fields) -> { - TypeName typeName = newTypeName(objectTypeName).build(); - InlineFragment inlineFragment = newInlineFragment() - .typeCondition(typeName) - .selectionSet(selectionSet(fields)) - .build(); - selections.add(inlineFragment); + fieldsByFragmentDetails.forEach((typeAndDeferPair, fields) -> { + InlineFragment.Builder fragmentBuilder = newInlineFragment() + .selectionSet(selectionSet(fields)); + + if (typeAndDeferPair.typeName != null) { + TypeName typeName = newTypeName(typeAndDeferPair.typeName).build(); + fragmentBuilder.typeCondition(typeName); + } + + if (typeAndDeferPair.deferLabel != null) { + Directive.Builder deferBuilder = Directive.newDirective().name("defer"); + + if (typeAndDeferPair.deferLabel.getValue() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferLabel.getValue())).build()); + } + + fragmentBuilder.directive(deferBuilder.build()); + } + + + selections.add(fragmentBuilder.build()); }); return selections.build(); @@ -189,7 +224,7 @@ private static Map selectionForNormalizedField(GraphQLSchema sche Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField,normalizedFieldToQueryDirectives, variableAccumulator)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator)); } return groupedFields; @@ -230,9 +265,9 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, .alias(executableNormalizedField.getAlias()) .selectionSet(selectionSet) .arguments(arguments); - if(queryDirectives == null || queryDirectives.getImmediateAppliedDirectivesByField().isEmpty() ){ + if (queryDirectives == null || queryDirectives.getImmediateAppliedDirectivesByField().isEmpty()) { return builder.build(); - }else { + } else { List directives = queryDirectives.getImmediateAppliedDirectivesByField().keySet().stream().flatMap(field -> field.getDirectives().stream()).collect(Collectors.toList()); return builder .directives(directives) @@ -326,4 +361,27 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, return Assert.assertShouldNeverHappen("Unknown operation kind " + operationKind); } + // TODO: This name is terrible + public static class ExecutionFragmentDetails { + private final String typeName; + private final DeferLabel deferLabel; + + public ExecutionFragmentDetails(String typeName, DeferLabel deferLabel) { + this.typeName = typeName; + this.deferLabel = deferLabel; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExecutionFragmentDetails that = (ExecutionFragmentDetails) o; + return Objects.equals(typeName, that.typeName) && Objects.equals(deferLabel, that.deferLabel); + } + + @Override + public int hashCode() { + return Objects.hash(typeName, deferLabel); + } + } } diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 892a253238..7754d6de83 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -2,7 +2,7 @@ import graphql.ExperimentalApi; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; /** @@ -24,11 +24,11 @@ * } * } * - * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" + * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" */ @ExperimentalApi public class DeferExecution { - private final Set labels = new HashSet<>(); + private final Set labels = new LinkedHashSet<>(); public void addLabel(DeferLabel label) { this.labels.add(label); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 110a3353f0..8c2f844d7a 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -16,23 +16,25 @@ import spock.lang.Specification class ExecutableNormalizedOperationFactoryDeferTest extends Specification { String schema = """ directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD type Query { dog: Dog + animal: Animal + } + + interface Animal { + name: String } - type Dog { + type Dog implements Animal { name: String breed: String owner: Person - friends: [Dog] } type Person { firstname: String lastname: String - friends: [Person] } """ @@ -327,6 +329,39 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "nested defers - with named spreads"() { + given: + + String query = ''' + query q { + animal { + name + ... on Dog @defer(label:"dog-defer") { + owner { + firstname + ... @defer(label: "lastname-defer") { + lastname + } + } + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + 'Dog.name', + 'Dog.owner defer[dog-defer]', + 'Person.firstname', + 'Person.lastname defer[lastname-defer]', + ] + } + def "nesting defer blocks that would always result in no data are ignored"() { given: diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 2400fa8ca0..78d518281c 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -40,23 +40,24 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification String sdl = """ directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT - directive @stream(if: Boolean, label: String, initialCount: Int = 0) on FIELD type Query { dog: Dog + animal: Animal + } + + interface Animal { + name: String } - type Dog { + type Dog implements Animal { name: String breed: String owner: Person - friends: [Dog] } type Person { firstname: String - lastname: String - friends: [Person] } """ @@ -78,13 +79,177 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ - dog { - name - ... @defer(label: "breed-defer") { - breed - } + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } +} +''' + } + + def "simple defer with named spread"() { + String query = """ + query q { + dog { + name + ... on Dog @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } +} +''' + } + + def "multiple labels on the same field"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + ... @defer(label: "breed-defer-2") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + ... @defer(label: "breed-defer-2") { + breed + } + } +} +''' + } + + def "multiple defers with same label on the same field"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + ... @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } +} +''' + } + + def "multiple defers without label on the same field"() { + String query = """ + query q { + dog { + name + ... @defer { + breed + } + ... @defer { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + name + ... @defer { + breed } } +} +''' + } + + def "defer on type spread"() { + String query = """ + query q { + animal { + ... on Dog @defer { + breed + } + ... on Dog { + name + } + ... on Dog @defer(label: "owner-defer") { + owner { + firstname + } + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + name + ... on Dog @defer { + breed + } + ... on Dog @defer(label: "owner-defer") { + owner { + firstname + } + } + } +} ''' } From 8b68720953c8dbfbfa8784f16f9b3e55d63da895 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:08:01 +1100 Subject: [PATCH 03/33] Fix edge case with non-conditional fields in fragments --- ...tableNormalizedOperationToAstCompiler.java | 16 +-- ...izedOperationToAstCompilerDeferTest.groovy | 128 ++++++++++++++++++ 2 files changed, 135 insertions(+), 9 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 74250e80ae..27b97a89f6 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -175,14 +175,12 @@ private static List> subselectionsForNormalizedField(GraphQLSchema }); } else if (nf.getDeferExecution() != null) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) - .forEach((objectTypeName, field) -> - nf.getDeferExecution().getLabels().stream() - .map(DeferLabel::getValue) - .forEach(label -> fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) - .add(field)) - ); + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator); + nf.getDeferExecution().getLabels().stream() + .map(DeferLabel::getValue) + .forEach(label -> fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) + .add(field)); } else { selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator)); } @@ -362,7 +360,7 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, } // TODO: This name is terrible - public static class ExecutionFragmentDetails { + private static class ExecutionFragmentDetails { private final String typeName; private final DeferLabel deferLabel; diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 78d518281c..fc090eb9a4 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -56,8 +56,21 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification owner: Person } + type Cat implements Animal { + name: String + breed: String + color: String + siblings: [Cat] + } + + type Fish implements Animal { + name: String + } + type Person { firstname: String + lastname: String + bestFriend: Person } """ @@ -253,6 +266,121 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "2 fragments on non-conditional fields"() { + String query = """ + query q { + animal { + ... on Cat @defer { + name + } + ... on Animal @defer { + name + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... @defer { + name + } + } +} +''' + } + + def "2 fragments on conditional fields"() { + String query = """ + query q { + animal { + ... on Cat @defer { + breed + } + ... on Dog @defer { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + name + } +} +''' + } + + def "nested defer"() { + String query = """ + query q { + animal { + ... on Cat @defer { + name + } + ... on Animal @defer { + name + ... on Dog @defer { + owner { + firstname + ... @defer { + lastname + } + ... @defer { + bestFriend { + firstname + ... @defer { + lastname + } + } + } + } + } + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... @defer { + name + } + ... on Dog @defer { + owner { + firstname + ... @defer { + bestFriend { + firstname + ... @defer { + lastname + } + } + lastname + } + } + } + } +} +''' + } + private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) From c85cbed61ad1a99f01e499df03a7b7c667f2cacd Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:23:13 +1100 Subject: [PATCH 04/33] Fix comment and test --- ...tableNormalizedOperationToAstCompiler.java | 2 +- ...izedOperationToAstCompilerDeferTest.groovy | 25 +++++++------------ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 27b97a89f6..9ae035e975 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -152,7 +152,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); - // All conditional fields go here instead of directly to selections, so they can be grouped together + // All conditional and deferred fields go here instead of directly to selections, so they can be grouped together // in the same inline fragment in the output // Map> fieldsByFragmentDetails = new LinkedHashMap<>(); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index fc090eb9a4..ac313785a0 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -24,20 +24,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } } - VariablePredicate jsonVariables = new VariablePredicate() { - @Override - boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { - "JSON" == normalizedInputValue.unwrappedTypeName && normalizedInputValue.value != null - } - } - - VariablePredicate allVariables = new VariablePredicate() { - @Override - boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { - return true - } - } - String sdl = """ directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT @@ -252,10 +238,12 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { - name ... on Dog @defer { breed } + ... on Dog { + name + } ... on Dog @defer(label: "owner-defer") { owner { firstname @@ -316,7 +304,12 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { - name + ... on Cat @defer { + breed + } + ... on Dog @defer { + breed + } } } ''' From df767cb66861197fea441da64eb29015efc84c59 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:31:28 +1100 Subject: [PATCH 05/33] Remove commented out code --- src/main/java/graphql/Directives.java | 33 +-------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 61358c5b68..98ca5b5625 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -42,14 +42,6 @@ public class Directives { @ExperimentalApi public static final DirectiveDefinition ONE_OF_DIRECTIVE_DEFINITION; - /** - * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in - * directive that is not available unless it is explicitly put into the schema. - */ -// @ExperimentalApi -// public static final DirectiveDefinition DEFER_DIRECTIVE_DEFINITION; - - static { DEPRECATED_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() .name(DEPRECATED) @@ -84,36 +76,13 @@ public class Directives { .directiveLocation(newDirectiveLocation().name(INPUT_OBJECT.name()).build()) .description(createDescription("Indicates an Input Object is a OneOf Input Object.")) .build(); - -// DEFER_DIRECTIVE_DEFINITION = DirectiveDefinition.newDirectiveDefinition() -// .name(DEFER) -// .description(createDescription("This directive allows results to be deferred during execution")) -// .directiveLocation(newDirectiveLocation().name(FRAGMENT_SPREAD.name()).build()) -// .directiveLocation(newDirectiveLocation().name(INLINE_FRAGMENT.name()).build()) -// .inputValueDefinition( -// newInputValueDefinition() -// .name("if") -// .description(createDescription("Deferred behaviour is controlled by this argument")) -// .type(newTypeName().name("Boolean").build()) -// .defaultValue(BooleanValue.newBooleanValue(true).build()) -// .build()) -// .inputValueDefinition( -// newInputValueDefinition() -// // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: -// // > `label` must not be provided as a variable. -// // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query -// .name("label") -// .description(createDescription("A unique label that represents the fragment being deferred")) -// .type(newTypeName().name("Boolean").build()) -// .defaultValue(BooleanValue.newBooleanValue(true).build()) -// .build()) -// .build(); } /** * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in * directive that is not available unless it is explicitly put into the schema. */ + @ExperimentalApi public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() .name("defer") .description("This directive allows results to be deferred during execution") From 7c047e23f0dfe721434746d5439cce2cd8205558 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 14:36:32 +1100 Subject: [PATCH 06/33] Fix assert error message --- .../java/graphql/normalized/incremental/IncrementalNodes.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 4763c6c9d2..6f0bb219c4 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -40,7 +40,7 @@ public DeferLabel getDeferLabel( return new DeferLabel(null); } - Assert.assertTrue(label instanceof String, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); return new DeferLabel((String) label); From 80dc0933bcbe9957c234c4397e47bb14cdcd3cae Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 12 Dec 2023 15:13:19 +1100 Subject: [PATCH 07/33] Add tests for 'if' argument --- ...NormalizedOperationFactoryDeferTest.groovy | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 8c2f844d7a..0b426f7e2d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -1,6 +1,6 @@ package graphql.normalized -import graphql.AssertException + import graphql.ExecutionInput import graphql.GraphQL import graphql.TestUtil @@ -390,6 +390,64 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "'if' argument is respected"() { + given: + + String query = ''' + query q { + dog { + ... @defer(if: false, label: "name-defer") { + name + } + + ... @defer(if: true, label: "another-name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[another-name-defer]', + ] + } + + def "'if' argument with different values on same field and same label"() { + given: + + String query = ''' + query q { + dog { + ... @defer(if: false, label: "name-defer") { + name + } + + ... @defer(if: true, label: "name-defer") { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[name-defer]', + ] + } + private List executeQueryAndPrintTree(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) @@ -416,7 +474,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = field.getDeferExecution().labels - .collect {it.value} + .collect { it.value } .join(",") return " defer[" + deferLabels + "]" From 407efbaf1ee3009141120ed3e25d08c5500eb0b0 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 13 Dec 2023 11:10:20 +1100 Subject: [PATCH 08/33] Add dedicated entry point for defer support --- ...tableNormalizedOperationToAstCompiler.java | 130 +++++++++++++++-- ...izedOperationToAstCompilerDeferTest.groovy | 20 +-- ...ormalizedOperationToAstCompilerTest.groovy | 137 ++++++++++++------ 3 files changed, 220 insertions(+), 67 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 9ae035e975..ffecf5a62a 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.directives.QueryDirectives; import graphql.introspection.Introspection; @@ -124,10 +125,69 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, false); + } + + + /** + * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s, with support for the experimental @defer directive. + *

+ * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable + * OR inlined into the operation text as a graphql literal. + * + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @return a {@link CompilerResult} object + * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, VariablePredicate) + */ + @ExperimentalApi + public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLSchema schema, + @NotNull OperationDefinition.Operation operationKind, + @Nullable String operationName, + @NotNull List topLevelFields, + @Nullable VariablePredicate variablePredicate) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); + } + + /** + * This will compile an operation text {@link Document} with possibly variables from the given {@link ExecutableNormalizedField}s, with support for the experimental @defer directive. + *

+ * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable + * OR inlined into the operation text as a graphql literal. + * + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param normalizedFieldToQueryDirectives the map of normalized field to query directives + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @return a {@link CompilerResult} object + * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, Map, VariablePredicate) + */ + @ExperimentalApi + public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLSchema schema, + @NotNull OperationDefinition.Operation operationKind, + @Nullable String operationName, + @NotNull List topLevelFields, + @NotNull Map normalizedFieldToQueryDirectives, + @Nullable VariablePredicate variablePredicate) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, true); + } + + private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, + @NotNull OperationDefinition.Operation operationKind, + @Nullable String operationName, + @NotNull List topLevelFields, + @NotNull Map normalizedFieldToQueryDirectives, + @Nullable VariablePredicate variablePredicate, + boolean deferSupport) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); - List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator); + List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport); SelectionSet selectionSet = new SelectionSet(selections); OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition() @@ -149,7 +209,56 @@ private static List> subselectionsForNormalizedField(GraphQLSchema @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - VariableAccumulator variableAccumulator) { + VariableAccumulator variableAccumulator, + boolean deferSupport) { + if (deferSupport) { + return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); + } else { + return subselectionsForNormalizedFieldNoDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); + } + } + + private static List> subselectionsForNormalizedFieldNoDeferSupport(GraphQLSchema schema, + @NotNull String parentOutputType, + List executableNormalizedFields, + @NotNull Map normalizedFieldToQueryDirectives, + VariableAccumulator variableAccumulator) { + ImmutableList.Builder> selections = ImmutableList.builder(); + + // All conditional fields go here instead of directly to selections, so they can be grouped together + // in the same inline fragment in the output + Map> fieldsByTypeCondition = new LinkedHashMap<>(); + + for (ExecutableNormalizedField nf : executableNormalizedFields) { + if (nf.isConditional(schema)) { + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, false) + .forEach((objectTypeName, field) -> + fieldsByTypeCondition + .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) + .add(field)); + } else { + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, false)); + } + } + + fieldsByTypeCondition.forEach((objectTypeName, fields) -> { + TypeName typeName = newTypeName(objectTypeName).build(); + InlineFragment inlineFragment = newInlineFragment() + .typeCondition(typeName) + .selectionSet(selectionSet(fields)) + .build(); + selections.add(inlineFragment); + }); + + return selections.build(); + } + + + private static List> subselectionsForNormalizedFieldWithDeferSupport(GraphQLSchema schema, + @NotNull String parentOutputType, + List executableNormalizedFields, + @NotNull Map normalizedFieldToQueryDirectives, + VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); // All conditional and deferred fields go here instead of directly to selections, so they can be grouped together @@ -159,7 +268,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) .forEach((objectTypeName, field) -> { if (nf.getDeferExecution() == null) { fieldsByFragmentDetails @@ -175,14 +284,14 @@ private static List> subselectionsForNormalizedField(GraphQLSchema }); } else if (nf.getDeferExecution() != null) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator); + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true); nf.getDeferExecution().getLabels().stream() .map(DeferLabel::getValue) .forEach(label -> fieldsByFragmentDetails .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true)); } } @@ -218,11 +327,12 @@ private static List> subselectionsForNormalizedField(GraphQLSchema private static Map selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, - VariableAccumulator variableAccumulator) { + VariableAccumulator variableAccumulator, + boolean deferSupport) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport)); } return groupedFields; @@ -235,7 +345,8 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, String objectTypeName, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, - VariableAccumulator variableAccumulator) { + VariableAccumulator variableAccumulator, + boolean deferSupport) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); @@ -248,7 +359,8 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, fieldOutputType.getName(), executableNormalizedField.getChildren(), normalizedFieldToQueryDirectives, - variableAccumulator + variableAccumulator, + deferSupport ); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index ac313785a0..96156c8cfb 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -14,7 +14,7 @@ import spock.lang.Specification import static graphql.ExecutionInput.newExecutionInput import static graphql.language.OperationDefinition.Operation.QUERY -import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocument +import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocumentWithDeferSupport class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification { VariablePredicate noVariables = new VariablePredicate() { @@ -74,7 +74,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -102,7 +102,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -133,7 +133,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -167,7 +167,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -198,7 +198,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -233,7 +233,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -270,7 +270,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -299,7 +299,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -347,7 +347,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index dd074ce7a8..05494950b4 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -3,12 +3,13 @@ package graphql.normalized import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables +import graphql.execution.directives.QueryDirectives import graphql.language.AstPrinter -import graphql.language.Field -import graphql.language.OperationDefinition import graphql.language.AstSorter import graphql.language.Document +import graphql.language.Field import graphql.language.IntValue +import graphql.language.OperationDefinition import graphql.language.StringValue import graphql.parser.Parser import graphql.schema.GraphQLSchema @@ -22,8 +23,12 @@ import static graphql.language.OperationDefinition.Operation.MUTATION import static graphql.language.OperationDefinition.Operation.QUERY import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocument +import static graphql.normalized.ExecutableNormalizedOperationToAstCompiler.compileToDocumentWithDeferSupport + +abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specification { + static boolean deferSupport + -class ExecutableNormalizedOperationToAstCompilerTest extends Specification { VariablePredicate noVariables = new VariablePredicate() { @Override boolean shouldMakeVariable(ExecutableNormalizedField executableNormalizedField, String argName, NormalizedInputValue normalizedInputValue) { @@ -128,7 +133,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -199,7 +204,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -250,7 +255,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -331,7 +336,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -356,6 +361,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } """ } + def "test interface fields with different output types on the implementations 4"() { // Tests we don't consider File as a possible option for parent on animals def schema = TestUtil.schema(""" @@ -422,7 +428,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -517,7 +523,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -584,7 +590,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -641,7 +647,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -699,7 +705,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -766,7 +772,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -864,7 +870,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -962,7 +968,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = compileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1029,7 +1035,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1063,7 +1069,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1089,7 +1095,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, "My_Op23", fields, noVariables) + def result = localCompileToDocument(schema, QUERY, "My_Op23", fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1134,7 +1140,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1166,9 +1172,9 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } ''' GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query,["v":123]) + def fields = createNormalizedFields(schema, query, ["v": 123]) when: - def result = compileToDocument(schema, QUERY, null, fields, allVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, allVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1200,7 +1206,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1231,7 +1237,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, SUBSCRIPTION, null, fields, noVariables) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1242,7 +1248,6 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } - def "test query directive"() { def sdl = ''' type Query { @@ -1275,14 +1280,14 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { ''' GraphQLSchema schema = mkSchema(sdl) Document document = new Parser().parse(query) - ExecutableNormalizedOperation eno = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema,document, null,RawVariables.emptyVariables()) + ExecutableNormalizedOperation eno = ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) when: - def result = compileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) OperationDefinition operationDefinition = result.document.getDefinitionsOfType(OperationDefinition.class)[0] - def fooField = (Field)operationDefinition.selectionSet.children[0] - def nameField = (Field)fooField.selectionSet.children[0] + def fooField = (Field) operationDefinition.selectionSet.children[0] + def nameField = (Field) fooField.selectionSet.children[0] def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1327,7 +1332,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1372,7 +1377,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1418,7 +1423,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1438,6 +1443,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { } ''' } + def "test is conditional when there is only one interface implementation"() { def sdl = ''' type Query { @@ -1468,7 +1474,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1507,7 +1513,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: documentPrinted == '''{ @@ -1558,7 +1564,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: // Note: the typename field moves out of a fragment because AFoo is the only impl @@ -1609,7 +1615,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: // Note: the typename field moves out of a fragment because AFoo is the only impl @@ -1659,7 +1665,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { GraphQLSchema schema = TestUtil.schema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, null, fields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: // Note: the typename field moves out of a fragment because AFoo is the only impl @@ -1695,7 +1701,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, vars) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1727,7 +1733,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, vars) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1759,7 +1765,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, vars) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1789,7 +1795,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1819,7 +1825,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1849,7 +1855,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1879,7 +1885,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1916,7 +1922,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) def vars = result.variables @@ -1953,7 +1959,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1988,7 +1994,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, MUTATION, null, fields, noVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, noVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -2031,7 +2037,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query, variables) when: - def result = compileToDocument(schema, MUTATION, null, fields, jsonVariables) + def result = localCompileToDocument(schema, MUTATION, null, fields, jsonVariables) def documentPrinted = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -2104,7 +2110,7 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { def fields = createNormalizedFields(schema, query) when: - def result = compileToDocument(schema, QUERY, "named", fields, allVariables) + def result = localCompileToDocument(schema, QUERY, "named", fields, allVariables) def document = result.document def vars = result.variables def ast = AstPrinter.printAst(new AstSorter().sort(document)) @@ -2159,4 +2165,39 @@ class ExecutableNormalizedOperationToAstCompilerTest extends Specification { .wiringFactory(wiringFactory).build() TestUtil.schema(sdl, runtimeWiring) } + + private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( + GraphQLSchema schema, + OperationDefinition.Operation operationKind, + String operationName, + List topLevelFields, + VariablePredicate variablePredicate + ) { + return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate) + } + + private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( + GraphQLSchema schema, + OperationDefinition.Operation operationKind, + String operationName, + List topLevelFields, + Map normalizedFieldToQueryDirectives, + VariablePredicate variablePredicate) { + if (deferSupport) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) + } + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) + } +} + +class ExecutableNormalizedOperationToAstCompilerTestWithDeferSupport extends ExecutableNormalizedOperationToAstCompilerTest { + static { + deferSupport = true + } +} + +class ExecutableNormalizedOperationToAstCompilerTestNoDeferSupport extends ExecutableNormalizedOperationToAstCompilerTest { + static { + deferSupport = false + } } From 17f1fab605381690ff44bd0bf857ee520243015f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 13 Dec 2023 16:25:04 +1100 Subject: [PATCH 09/33] Add dedicated entrypoint with defer support in the ENFOperationFactory --- .../normalized/ExecutableNormalizedField.java | 4 + .../ExecutableNormalizedOperationFactory.java | 106 +++++++++--- ...NormalizedOperationFactoryDeferTest.groovy | 8 +- ...tableNormalizedOperationFactoryTest.groovy | 153 ++++++++++++------ ...izedOperationToAstCompilerDeferTest.groovy | 8 +- 5 files changed, 208 insertions(+), 71 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 73c1df4d0b..cc9f95594b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.Internal; import graphql.Mutable; import graphql.PublicApi; @@ -372,6 +373,7 @@ public String getSingleObjectTypeName() { * TODO Javadoc * @return */ + @ExperimentalApi public DeferExecution getDeferExecution() { return deferExecution; } @@ -670,6 +672,8 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } + + @ExperimentalApi public Builder deferExecution(DeferExecution deferExecution) { this.deferExecution = deferExecution; return this; diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 1af327ae39..07d8ca0199 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -173,7 +173,8 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( getOperationResult.fragmentsByName, coercedVariableValues, null, - Options.defaultOptions()); + Options.defaultOptions(), + false); } /** @@ -195,7 +196,8 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( fragments, coercedVariableValues, null, - Options.defaultOptions()); + Options.defaultOptions(), + false); } /** @@ -271,7 +273,40 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW getOperationResult.operationDefinition, getOperationResult.fragmentsByName, rawVariables, - options + options, + false + ); + } + + public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithDeferSupport( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + CoercedVariables coercedVariableValues + ) { + NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); + return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, + getOperationResult.operationDefinition, + getOperationResult.fragmentsByName, + coercedVariableValues, + null, + Options.defaultOptions(), + true); + } + + public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariablesWithDeferSupport(GraphQLSchema graphQLSchema, + Document document, + String operationName, + RawVariables rawVariables, + Options options) { + NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); + + return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, + getOperationResult.operationDefinition, + getOperationResult.fragmentsByName, + rawVariables, + options, + true ); } @@ -279,7 +314,8 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit OperationDefinition operationDefinition, Map fragments, RawVariables rawVariables, - Options options) { + Options options, + boolean deferSupport) { List variableDefinitions = operationDefinition.getVariableDefinitions(); CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, @@ -296,7 +332,8 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit fragments, coercedVariableValues, normalizedVariableValues, - options); + options, + deferSupport); } /** @@ -307,7 +344,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr Map fragments, CoercedVariables coercedVariableValues, @Nullable Map normalizedVariableValues, - Options options) { + Options options, + boolean deferSupport) { FieldCollectorNormalizedQueryParams parameters = FieldCollectorNormalizedQueryParams .newParameters() .fragments(fragments) @@ -349,7 +387,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr captureMergedField, coordinatesToNormalizedFields, 1, - options.getMaxChildrenDepth()); + options.getMaxChildrenDepth(), + deferSupport); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); @@ -374,12 +413,13 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz BiConsumer captureMergedField, ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, - int maxLevel) { + int maxLevel, + boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); } - CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1); + CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); @@ -398,7 +438,8 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz captureMergedField, coordinatesToNormalizedFields, curLevel + 1, - maxLevel); + maxLevel, + deferSupport); } } @@ -446,7 +487,8 @@ public CollectNFResult(Collection children, Immutable public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParams parameters, ExecutableNormalizedField executableNormalizedField, ImmutableList mergedField, - int level) { + int level, + boolean deferSupport) { List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { @@ -472,7 +514,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } @@ -498,7 +540,7 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, false); return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } @@ -508,10 +550,11 @@ private void createNFs(ImmutableList.Builder nfListBu Map> fieldsByName, ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, - ExecutableNormalizedField parent) { + ExecutableNormalizedField parent, + boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); - List commonParentsGroups = groupByCommonParents(fieldsWithSameResultKey); + List commonParentsGroups = groupByCommonParents(fieldsWithSameResultKey, deferSupport); for (CollectedFieldGroup fieldGroup : commonParentsGroups) { ExecutableNormalizedField nf = createNF(parameters, fieldGroup, level, parent); if (nf == null) { @@ -570,7 +613,33 @@ public CollectedFieldGroup(Set fields, Set ob } } - private List groupByCommonParents(Collection fields) { + private List groupByCommonParents(Collection fields, boolean deferSupport) { + if (deferSupport) { + return groupByCommonParentsWithDeferSupport(fields); + } else { + return groupByCommonParentsNoDeferSupport(fields); + } + } + + private List groupByCommonParentsNoDeferSupport(Collection fields) { + ImmutableSet.Builder objectTypes = ImmutableSet.builder(); + for (CollectedField collectedField : fields) { + objectTypes.addAll(collectedField.objectTypes); + } + Set allRelevantObjects = objectTypes.build(); + Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); + if (groupByAstParent.size() == 1) { + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, null)); + } + ImmutableList.Builder result = ImmutableList.builder(); + for (GraphQLObjectType objectType : allRelevantObjects) { + Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), null)); + } + return result.build(); + } + + private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); DeferExecution deferExecution = null; for (CollectedField collectedField : fields) { @@ -616,7 +685,7 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet } else if (selection instanceof InlineFragment) { collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition, deferLabel); } else if (selection instanceof FragmentSpread) { - collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects, deferLabel); + collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects); } } } @@ -647,8 +716,7 @@ public boolean isConcrete() { private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameters, List result, FragmentSpread fragmentSpread, - Set possibleObjects, - DeferLabel deferLabel + Set possibleObjects ) { if (!conditionalNodes.shouldInclude(fragmentSpread, parameters.getCoercedVariableValues(), diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 0b426f7e2d..76750b3cb7 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -453,7 +453,13 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + graphQLSchema, + document, + null, + RawVariables.of(variables), + ExecutableNormalizedOperationFactory.Options.defaultOptions(), + ) return printTreeWithIncrementalExecutionDetails(tree) } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 89ea656b81..72fa48b247 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -27,7 +27,10 @@ import static graphql.language.AstPrinter.printAst import static graphql.parser.Parser.parseValue import static graphql.schema.FieldCoordinates.coordinates -class ExecutableNormalizedOperationFactoryTest extends Specification { +abstract class ExecutableNormalizedOperationFactoryTest extends Specification { + static boolean deferSupport + + def "test"() { String schema = """ type Query{ @@ -112,8 +115,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -199,7 +201,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -279,7 +281,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -330,7 +332,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -373,7 +375,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -423,7 +425,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -486,7 +488,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -532,7 +534,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -576,7 +578,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -620,7 +622,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -652,7 +654,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -703,7 +705,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -753,7 +755,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -792,7 +794,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -836,7 +838,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -876,7 +878,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -924,7 +926,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1027,7 +1029,7 @@ type Dog implements Animal{ def subFooField = (document.getDefinitions()[1] as FragmentDefinition).getSelectionSet().getSelections()[0] as Field ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() expect: @@ -1070,7 +1072,7 @@ type Dog implements Animal{ def idField = petsField.getSelectionSet().getSelections()[0] as Field ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1119,7 +1121,7 @@ type Dog implements Animal{ def typeField = selections[3] as Field ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() expect: @@ -1176,7 +1178,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1219,7 +1221,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) expect: @@ -1247,7 +1249,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def normalizedFieldToMergedField = tree.getNormalizedFieldToMergedField() Traverser traverser = Traverser.depthFirst({ it.getChildren() }) List result = new ArrayList<>() @@ -1287,7 +1289,7 @@ type Dog implements Animal{ ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def coordinatesToNormalizedFields = tree.coordinatesToNormalizedFields then: @@ -1386,7 +1388,7 @@ schema { Document document = TestUtil.parseQuery(mutation) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -1443,7 +1445,7 @@ schema { // the normalized arg value should be the same regardless of how the value was provided def expectedNormalizedArgValue = [foo: new NormalizedInputValue("String", parseValue('"foo"')), input2: new NormalizedInputValue("Input2", [bar: new NormalizedInputValue("Int", parseValue("123"))])] when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def secondField = topLevelField.getChildren().get(0) def arg1 = secondField.getNormalizedArgument("arg1") @@ -1484,7 +1486,7 @@ schema { def document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) then: def topLevelField = tree.getTopLevelFields().get(0) @@ -1523,7 +1525,7 @@ schema { otherVar: null, ] when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) then: def topLevelField = tree.getTopLevelFields().get(0) @@ -1575,7 +1577,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def arg1 = topLevelField.getNormalizedArgument("arg1") def arg2 = topLevelField.getNormalizedArgument("arg2") @@ -1628,7 +1630,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def topLevelField = tree.getTopLevelFields().get(0) def arg1 = topLevelField.getNormalizedArgument("arg1") def arg2 = topLevelField.getNormalizedArgument("arg2") @@ -1683,7 +1685,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) then: tree.normalizedFieldToMergedField.size() == 3 @@ -1741,7 +1743,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) then: @@ -1789,7 +1791,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) then: @@ -1865,7 +1867,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -1929,7 +1931,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -1986,7 +1988,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2061,7 +2063,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2123,7 +2125,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2165,7 +2167,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2208,7 +2210,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2251,7 +2253,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2326,7 +2328,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2402,7 +2404,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(schema, document, null, RawVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, schema) then: @@ -2464,7 +2466,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) println String.join("\n", printTree(tree)) def printedTree = printTree(tree) @@ -2521,7 +2523,7 @@ schema { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() when: - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) + def tree = localCreateExecutableNormalizedOperationWithRawVariables(graphQLSchema, document, null, RawVariables.of(variables)) def printedTree = printTreeAndDirectives(tree) then: @@ -2585,7 +2587,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2638,7 +2640,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2685,7 +2687,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) def dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) expect: @@ -2875,4 +2877,55 @@ fragment personName on Person { then: noExceptionThrown() } + + private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperation( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + CoercedVariables coercedVariableValues + ) { + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + if (deferSupport) { + return dependencyGraph.createExecutableNormalizedOperationWithDeferSupport(graphQLSchema, document, operationName, coercedVariableValues) + } else { + return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues) + } + } + + private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + RawVariables rawVariables + ) { + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + if (deferSupport) { + return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + graphQLSchema, + document, + operationName, + rawVariables, + ExecutableNormalizedOperationFactory.Options.defaultOptions() + ) + } else { + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + operationName, + rawVariables + ) + } + } +} + +class ExecutableNormalizedOperationFactoryTestWithDeferSupport extends ExecutableNormalizedOperationFactoryTest { + static { + deferSupport = true + } +} + +class ExecutableNormalizedOperationFactoryTestNoDeferSupport extends ExecutableNormalizedOperationFactoryTest { + static { + deferSupport = false + } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 96156c8cfb..5eb640508d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -379,7 +379,13 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification Document originalDocument = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) + return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + schema, + originalDocument, + null, + RawVariables.of(variables), + ExecutableNormalizedOperationFactory.Options.defaultOptions() + ) } private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { From 86f9dbe46a87b42d5c3660c14fd6231a94da29f7 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 15 Dec 2023 09:33:08 +1100 Subject: [PATCH 10/33] Add Javadocs --- .../ExecutableNormalizedOperationFactory.java | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 07d8ca0199..120da83ad3 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import graphql.ExperimentalApi; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.collect.ImmutableKit; @@ -278,6 +279,22 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW ); } + + /** + * This will create a runtime representation of the graphql operation that would be executed + * in a runtime sense. + *

+ * This version of the "createExecutableNormalizedOperation" method has support for the `@defer` directive, + * which is still a experimental feature in GraphQL Java. + * + * @param graphQLSchema the schema to be used + * @param document the {@link Document} holding the operation text + * @param operationName the operation name to use + * @param coercedVariableValues the coerced variables to use + * @return a runtime representation of the graphql operation. + * @see ExecutableNormalizedOperationFactory#createExecutableNormalizedOperation(GraphQLSchema, Document, String, CoercedVariables) + */ + @ExperimentalApi public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithDeferSupport( GraphQLSchema graphQLSchema, Document document, @@ -294,11 +311,27 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW true); } + /** + * This will create a runtime representation of the graphql operation that would be executed + * in a runtime sense. + * + *

+ * This version of the "createExecutableNormalizedOperationWithRawVariables" method has support for the `@defer` + * directive, which is still a experimental feature in GraphQL Java. + * + * @param graphQLSchema the schema to be used + * @param document the {@link Document} holding the operation text + * @param operationName the operation name to use + * @param rawVariables the raw variables that have not yet been coerced + * @param options the {@link Options} to use for parsing + * @return a runtime representation of the graphql operation. + */ + @ExperimentalApi public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariablesWithDeferSupport(GraphQLSchema graphQLSchema, - Document document, - String operationName, - RawVariables rawVariables, - Options options) { + Document document, + String operationName, + RawVariables rawVariables, + Options options) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, From 0f4abf4565822148253b1bdbe2f058338d26f329 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 15 Dec 2023 10:15:48 +1100 Subject: [PATCH 11/33] Lots of small adjustments --- src/main/java/graphql/Directives.java | 7 ++--- .../normalized/ExecutableNormalizedField.java | 17 ++++------- ...tableNormalizedOperationToAstCompiler.java | 7 +++-- .../normalized/incremental/DeferLabel.java | 5 ++++ ...NormalizedOperationFactoryDeferTest.groovy | 29 +++++++++++++++++++ 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 98ca5b5625..fe207f47c8 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -79,8 +79,8 @@ public class Directives { } /** - * The @defer directive can be used to defer sending data for a field till later in the query. This is an opt-in - * directive that is not available unless it is explicitly put into the schema. + * The @defer directive can be used to defer sending data for a fragment until later in the query. + * This is an opt-in directive that is not available unless it is explicitly put into the schema. */ @ExperimentalApi public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() @@ -94,9 +94,6 @@ public class Directives { .defaultValueLiteral(BooleanValue.newBooleanValue(true).build()) ) .argument(newArgument() - // NOTE: as per the spec draft [https://github.com/graphql/graphql-spec/pull/742/files]: - // > `label` must not be provided as a variable. - // VALIDATION: the value of "label" MUST be unique across @defer and @stream in a query .name("label") .type(GraphQLString) .description("A unique label that represents the fragment being deferred") diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index cc9f95594b..12ab96e87d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -265,7 +265,6 @@ public void clearChildren() { * WARNING: This is not always the key in the execution result, because of possible field aliases. * * @return the name of this {@link ExecutableNormalizedField} - * * @see #getResultKey() * @see #getAlias() */ @@ -275,7 +274,6 @@ public String getName() { /** * @return the same value as {@link #getName()} - * * @see #getResultKey() * @see #getAlias() */ @@ -288,7 +286,6 @@ public String getFieldName() { * This is either a field alias or the value of {@link #getName()} * * @return the result key for this {@link ExecutableNormalizedField}. - * * @see #getName() */ public String getResultKey() { @@ -300,7 +297,6 @@ public String getResultKey() { /** * @return the field alias used or null if there is none - * * @see #getResultKey() * @see #getName() */ @@ -319,7 +315,6 @@ public ImmutableList getAstArguments() { * Returns an argument value as a {@link NormalizedInputValue} which contains its type name and its current value * * @param name the name of the argument - * * @return an argument value */ public NormalizedInputValue getNormalizedArgument(String name) { @@ -368,12 +363,15 @@ public String getSingleObjectTypeName() { return objectTypeNames.iterator().next(); } - /** - * TODO Javadoc - * @return + * Returns an object containing the details about the defer aspect of this field's execution. + *

+ * "null" is returned when this field is not supposed to be deferred. + * + * @return details about the defer execution */ @ExperimentalApi + @Nullable public DeferExecution getDeferExecution() { return deferExecution; } @@ -427,7 +425,6 @@ public List getChildren() { * Returns the list of child fields that would have the same result key * * @param resultKey the result key to check - * * @return a list of all direct {@link ExecutableNormalizedField} children with the specified result key */ public List getChildrenWithSameResultKey(String resultKey) { @@ -448,7 +445,6 @@ public List getChildren(int includingRelativeLevel) { * This returns the child fields that can be used if the object is of the specified object type * * @param objectTypeName the object type - * * @return a list of child fields that would apply to that object type */ public List getChildren(String objectTypeName) { @@ -581,7 +577,6 @@ public static Builder newNormalizedField() { * Allows this {@link ExecutableNormalizedField} to be transformed via a {@link Builder} consumer callback * * @param builderConsumer the consumer given a builder - * * @return a new transformed {@link ExecutableNormalizedField} */ public ExecutableNormalizedField transform(Consumer builderConsumer) { diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index ffecf5a62a..63bc5a8bf5 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; +import graphql.Directives; import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.directives.QueryDirectives; @@ -305,7 +306,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor } if (typeAndDeferPair.deferLabel != null) { - Directive.Builder deferBuilder = Directive.newDirective().name("defer"); + Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); if (typeAndDeferPair.deferLabel.getValue() != null) { deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferLabel.getValue())).build()); @@ -471,7 +472,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, return Assert.assertShouldNeverHappen("Unknown operation kind " + operationKind); } - // TODO: This name is terrible + /** + * Represents important execution details that can be associated with a fragment. + */ private static class ExecutionFragmentDetails { private final String typeName; private final DeferLabel deferLabel; diff --git a/src/main/java/graphql/normalized/incremental/DeferLabel.java b/src/main/java/graphql/normalized/incremental/DeferLabel.java index d965e5636d..4c6249bd62 100644 --- a/src/main/java/graphql/normalized/incremental/DeferLabel.java +++ b/src/main/java/graphql/normalized/incremental/DeferLabel.java @@ -1,11 +1,16 @@ package graphql.normalized.incremental; +import graphql.ExperimentalApi; + import javax.annotation.Nullable; import java.util.Objects; /** * Holds the value of the 'label' argument of a defer directive. + *

+ * 'value' can be 'null', as 'label' is not a required argument on the '@defer' directive. */ +@ExperimentalApi public class DeferLabel { private final String value; diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 76750b3cb7..3d5f809fa1 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -419,6 +419,35 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "'if' argument is respected when value is passed through variable"() { + given: + + String query = ''' + query q($if1: Boolean, $if2: Boolean) { + dog { + ... @defer(if: $if1, label: "name-defer") { + name + } + + ... @defer(if: $if2, label: "another-name-defer") { + name + } + } + } + + ''' + + Map variables = [if1: false, if2: true] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[another-name-defer]', + ] + } + def "'if' argument with different values on same field and same label"() { given: From 444f775ec13736952945f97dc7c75d41fd107717 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 15 Dec 2023 17:32:25 +1100 Subject: [PATCH 12/33] Add test case for multiple defers at the same level --- ...izedOperationToAstCompilerDeferTest.groovy | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 5eb640508d..fe8e98116a 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -374,6 +374,48 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "multiple defers at the same level are preserved"() { + String query = """ + query q { + dog { + ... @defer { + name + } + ... @defer { + breed + } + ... @defer { + owner { + firstname + } + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def fields = createNormalizedFields(schema, query) + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + ... @defer { + name + } + ... @defer { + breed + } + ... @defer { + owner { + firstname + } + } + } + } +''' + } + private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { assertValidQuery(schema, query, variables) Document originalDocument = TestUtil.parseQuery(query) From 32881a30b1fde07f2fe24fd27a335804fd9a62ea Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Mon, 18 Dec 2023 15:47:31 +1100 Subject: [PATCH 13/33] Moving over to new implementation: changed method signatures --- .../java/graphql/DeferredExecutionResult.java | 16 ++ .../graphql/DeferredExecutionResultImpl.java | 68 +++++++++ .../graphql/execution/FieldCollector.java | 2 +- .../graphql/execution/defer/DeferSupport.java | 82 ++++++++++ .../graphql/execution/defer/DeferredCall.java | 43 ++++++ .../execution/defer/DeferredErrorSupport.java | 31 ++++ .../DeferredFieldInstrumentationContext.java | 12 ++ ...nstrumentationDeferredFieldParameters.java | 25 ++++ .../ExecutableNormalizedOperation.java | 6 +- .../ExecutableNormalizedOperationFactory.java | 77 +++++----- ...tableNormalizedOperationToAstCompiler.java | 84 +++++------ .../incremental/DeferExecution.java | 34 +---- .../incremental/IncrementalNodes.java | 36 ++++- .../execution/FieldCollectorTest.groovy | 140 +++++++++++++++++- ...izedOperationToAstCompilerDeferTest.groovy | 20 +-- ...ormalizedOperationToAstCompilerTest.groovy | 2 +- 16 files changed, 547 insertions(+), 131 deletions(-) create mode 100644 src/main/java/graphql/DeferredExecutionResult.java create mode 100644 src/main/java/graphql/DeferredExecutionResultImpl.java create mode 100644 src/main/java/graphql/execution/defer/DeferSupport.java create mode 100644 src/main/java/graphql/execution/defer/DeferredCall.java create mode 100644 src/main/java/graphql/execution/defer/DeferredErrorSupport.java create mode 100644 src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java create mode 100644 src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java diff --git a/src/main/java/graphql/DeferredExecutionResult.java b/src/main/java/graphql/DeferredExecutionResult.java new file mode 100644 index 0000000000..18eb23c74a --- /dev/null +++ b/src/main/java/graphql/DeferredExecutionResult.java @@ -0,0 +1,16 @@ +package graphql; + +import java.util.List; + +/** + * Results that come back from @defer fields have an extra path property that tells you where + * that deferred result came in the original query + */ +@PublicApi +public interface DeferredExecutionResult extends ExecutionResult { + + /** + * @return the execution path of this deferred result in the original query + */ + List getPath(); +} diff --git a/src/main/java/graphql/DeferredExecutionResultImpl.java b/src/main/java/graphql/DeferredExecutionResultImpl.java new file mode 100644 index 0000000000..b5a57eadd4 --- /dev/null +++ b/src/main/java/graphql/DeferredExecutionResultImpl.java @@ -0,0 +1,68 @@ +package graphql; + +import graphql.execution.ResultPath; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static graphql.Assert.assertNotNull; + +/** + * Results that come back from @defer fields have an extra path property that tells you where + * that deferred result came in the original query + */ +@PublicApi +public class DeferredExecutionResultImpl extends ExecutionResultImpl implements DeferredExecutionResult { + + private final List path; + + private DeferredExecutionResultImpl(List path, ExecutionResultImpl executionResult) { + super(executionResult); + this.path = assertNotNull(path); + } + + /** + * @return the execution path of this deferred result in the original query + */ + public List getPath() { + return path; + } + + @Override + public Map toSpecification() { + Map map = new LinkedHashMap<>(super.toSpecification()); + map.put("path", path); + return map; + } + + public static Builder newDeferredExecutionResult() { + return new Builder(); + } + + public static class Builder { + private List path = Collections.emptyList(); + private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); + + public Builder path(ResultPath path) { + this.path = assertNotNull(path).toList(); + return this; + } + + public Builder from(ExecutionResult executionResult) { + builder.from(executionResult); + return this; + } + + public Builder addErrors(List errors) { + builder.addErrors(errors); + return this; + } + + public DeferredExecutionResult build() { + ExecutionResultImpl build = (ExecutionResultImpl) builder.build(); + return new DeferredExecutionResultImpl(path, build); + } + } +} diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index a6f1310a8c..8fae8a3afb 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -25,7 +25,7 @@ /** * A field collector can iterate over field selection sets and build out the sub fields that have been selected, - * expanding named and inline fragments as it goes.s + * expanding named and inline fragments as it goes. */ @Internal public class FieldCollector { diff --git a/src/main/java/graphql/execution/defer/DeferSupport.java b/src/main/java/graphql/execution/defer/DeferSupport.java new file mode 100644 index 0000000000..3bc777b8c5 --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferSupport.java @@ -0,0 +1,82 @@ +package graphql.execution.defer; + +import graphql.DeferredExecutionResult; +import graphql.Directives; +import graphql.ExecutionResult; +import graphql.Internal; +import graphql.execution.MergedField; +import graphql.execution.ValuesResolver; +import graphql.execution.reactive.SingleSubscriberPublisher; +import graphql.language.Directive; +import graphql.language.Field; +import org.reactivestreams.Publisher; + +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +import static graphql.Directives.*; + +/** + * This provides support for @defer directives on fields that mean that results will be sent AFTER + * the main result is sent via a Publisher stream. + */ +@Internal +public class DeferSupport { + + private final AtomicBoolean deferDetected = new AtomicBoolean(false); + private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); + private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); +// private final ValuesResolver valuesResolver = new ValuesResolver(); + +// public boolean checkForDeferDirective(MergedField currentField, Map variables) { +// for (Field field : currentField.getFields()) { +// Directive directive = field.getDirective(DeferDirective.getName()); +// if (directive != null) { +// Map argumentValues = valuesResolver.getArgumentValues(DeferDirective.getArguments(), directive.getArguments(), variables); +// return (Boolean) argumentValues.get("if"); +// } +// } +// return false; +// } +// +// @SuppressWarnings("FutureReturnValueIgnored") +// private void drainDeferredCalls() { +// if (deferredCalls.isEmpty()) { +// publisher.noMoreData(); +// return; +// } +// DeferredCall deferredCall = deferredCalls.pop(); +// CompletableFuture future = deferredCall.invoke(); +// future.whenComplete((executionResult, exception) -> { +// if (exception != null) { +// publisher.offerError(exception); +// return; +// } +// publisher.offer(executionResult); +// drainDeferredCalls(); +// }); +// } +// +// public void enqueue(DeferredCall deferredCall) { +// deferDetected.set(true); +// deferredCalls.offer(deferredCall); +// } +// +// public boolean isDeferDetected() { +// return deferDetected.get(); +// } +// +// /** +// * When this is called the deferred execution will begin +// * +// * @return the publisher of deferred results +// */ +// public Publisher startDeferredCalls() { +// drainDeferredCalls(); +// return publisher; +// } +} diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java new file mode 100644 index 0000000000..b52e124d5b --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferredCall.java @@ -0,0 +1,43 @@ +package graphql.execution.defer; + +import graphql.DeferredExecutionResult; +import graphql.DeferredExecutionResultImpl; +import graphql.ExecutionResult; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.execution.ResultPath; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * This represents a deferred call (aka @defer) to get an execution result sometime after + * the initial query has returned + */ +@Internal +public class DeferredCall { + private final ResultPath path; + private final Supplier> call; + private final DeferredErrorSupport errorSupport; + + public DeferredCall(ResultPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { + this.path = path; + this.call = call; + this.errorSupport = deferredErrorSupport; + } + + CompletableFuture invoke() { + CompletableFuture future = call.get(); + return future.thenApply(this::transformToDeferredResult); + } + + private DeferredExecutionResult transformToDeferredResult(ExecutionResult executionResult) { + List errorsEncountered = errorSupport.getErrors(); + DeferredExecutionResultImpl.Builder builder = DeferredExecutionResultImpl.newDeferredExecutionResult().from(executionResult); + return builder + .addErrors(errorsEncountered) + .path(path) + .build(); + } +} diff --git a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java new file mode 100644 index 0000000000..3ef4f34c5d --- /dev/null +++ b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java @@ -0,0 +1,31 @@ +package graphql.execution.defer; + +import graphql.ExceptionWhileDataFetching; +import graphql.GraphQLError; +import graphql.Internal; +import graphql.execution.ExecutionStrategyParameters; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This captures errors that occur while a deferred call is being made + */ +@Internal +public class DeferredErrorSupport { + + private final List errors = new CopyOnWriteArrayList<>(); + + public void onFetchingException(ExecutionStrategyParameters parameters, Throwable e) { + ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(parameters.getPath(), e, parameters.getField().getSingleField().getSourceLocation()); + onError(error); + } + + public void onError(GraphQLError gError) { + errors.add(gError); + } + + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java new file mode 100644 index 0000000000..39de62be19 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java @@ -0,0 +1,12 @@ +package graphql.execution.instrumentation; + +import graphql.ExecutionResult; +import graphql.execution.FieldValueInfo; + +public interface DeferredFieldInstrumentationContext extends InstrumentationContext { + + default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { + + } + +} diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java new file mode 100644 index 0000000000..e71d104438 --- /dev/null +++ b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java @@ -0,0 +1,25 @@ +package graphql.execution.instrumentation.parameters; + +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.ExecutionStrategyParameters; + +import java.util.function.Supplier; + +/** + * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods + */ +public class InstrumentationDeferredFieldParameters extends InstrumentationFieldParameters { + + private final ExecutionStrategyParameters executionStrategyParameters; + + + public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, ExecutionStrategyParameters executionStrategyParameters) { + super(executionContext, executionStepInfo); + this.executionStrategyParameters = executionStrategyParameters; + } + + public ExecutionStrategyParameters getExecutionStrategyParameters() { + return executionStrategyParameters; + } +} diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index ce50c9931b..83cebdb77f 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -8,6 +8,7 @@ import graphql.execution.directives.QueryDirectives; import graphql.language.Field; import graphql.language.OperationDefinition; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLFieldsContainer; @@ -30,6 +31,7 @@ public class ExecutableNormalizedOperation { private final ImmutableListMultimap fieldToNormalizedField; private final Map normalizedFieldToMergedField; private final Map normalizedFieldToQueryDirectives; + private final ImmutableListMultimap normalizedFieldToDeferExecution; private final ImmutableListMultimap coordinatesToNormalizedFields; public ExecutableNormalizedOperation( @@ -39,7 +41,8 @@ public ExecutableNormalizedOperation( ImmutableListMultimap fieldToNormalizedField, Map normalizedFieldToMergedField, Map normalizedFieldToQueryDirectives, - ImmutableListMultimap coordinatesToNormalizedFields + ImmutableListMultimap coordinatesToNormalizedFields, + ImmutableListMultimap normalizedFieldToDeferExecution ) { this.operation = operation; this.operationName = operationName; @@ -48,6 +51,7 @@ public ExecutableNormalizedOperation( this.normalizedFieldToMergedField = normalizedFieldToMergedField; this.normalizedFieldToQueryDirectives = normalizedFieldToQueryDirectives; this.coordinatesToNormalizedFields = coordinatesToNormalizedFields; + this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } /** diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 120da83ad3..081146202b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -28,7 +28,6 @@ import graphql.language.SelectionSet; import graphql.language.VariableDefinition; import graphql.normalized.incremental.DeferExecution; -import graphql.normalized.incremental.DeferLabel; import graphql.normalized.incremental.IncrementalNodes; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; @@ -434,7 +433,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr fieldToNormalizedField.build(), normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), - coordinatesToNormalizedFields.build() + coordinatesToNormalizedFields.build(), + collectFromOperationResult.normalizedFieldToDeferExecution ); } @@ -509,10 +509,12 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; + private final ImmutableListMultimap normalizedFieldToDeferExecution; - public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields) { + public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields, ImmutableListMultimap normalizedFieldToDeferExecution) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; + this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -525,7 +527,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableListMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -546,10 +548,11 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); + ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -572,10 +575,11 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); + ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, false); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, false); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } private void createNFs(ImmutableList.Builder nfListBuilder, @@ -584,6 +588,7 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, + ImmutableListMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -630,19 +635,18 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p .fieldName(fieldName) .level(level) .parent(parent) - .deferExecution(collectedFieldGroup.deferExecution) .build(); } private static class CollectedFieldGroup { Set objectTypes; Set fields; - DeferExecution deferExecution; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, DeferExecution deferExecution) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; - this.deferExecution = deferExecution; + this.deferExecutions = deferExecutions; } } @@ -674,32 +678,29 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - DeferExecution deferExecution = null; + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferLabel collectedDeferLabel = collectedField.deferLabel; - - if (collectedDeferLabel == null) { - continue; - } + DeferExecution collectedDeferExecution = collectedField.deferExecution; - if (deferExecution == null) { - deferExecution = new DeferExecution(); + if (collectedDeferExecution != null) { + deferExecutionsBuilder.add(collectedDeferExecution); } - - deferExecution.addLabel(collectedDeferLabel); } Set allRelevantObjects = objectTypes.build(); + Set deferExecutions = deferExecutionsBuilder.build(); + Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecution)); + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecutions)); } ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecution)); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecutions)); } return result.build(); } @@ -710,13 +711,13 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferLabel deferLabel + DeferExecution deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { - collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition, deferLabel); + collectField(parameters, result, (Field) selection, possibleObjects, astTypeCondition, deferExecution); } else if (selection instanceof InlineFragment) { - collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition, deferLabel); + collectInlineFragment(parameters, result, (InlineFragment) selection, possibleObjects, astTypeCondition); } else if (selection instanceof FragmentSpread) { collectFragmentSpread(parameters, result, (FragmentSpread) selection, possibleObjects); } @@ -727,14 +728,13 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; + DeferExecution deferExecution; - DeferLabel deferLabel; - - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferLabel deferLabel) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; - this.deferLabel = deferLabel; + this.deferExecution = deferExecution; } public boolean isAbstract() { @@ -766,11 +766,11 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); + collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -778,8 +778,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter List result, InlineFragment inlineFragment, Set possibleObjects, - GraphQLCompositeType astTypeCondition, - DeferLabel deferLabel + GraphQLCompositeType astTypeCondition ) { if (!conditionalNodes.shouldInclude(inlineFragment, parameters.getCoercedVariableValues(), parameters.getGraphQLSchema(), parameters.getGraphQLContext())) { return; @@ -793,9 +792,9 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter } - DeferLabel newDeferLabel = incrementalNodes.getDeferLabel(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); - collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferLabel); + collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } private void collectField(FieldCollectorNormalizedQueryParams parameters, @@ -803,7 +802,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferLabel deferLabel + DeferExecution deferExecution ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), @@ -815,7 +814,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, if (possibleObjectTypes.isEmpty()) { return; } - result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferLabel)); + result.add(new CollectedField(field, possibleObjectTypes, astTypeCondition, deferExecution)); } private Set narrowDownPossibleObjects(Set currentOnes, diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 63bc5a8bf5..1737eb9dec 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,7 +23,7 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferLabel; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -126,7 +126,7 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, false); + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, null); } @@ -149,8 +149,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull OperationDefinition.Operation operationKind, @Nullable String operationName, @NotNull List topLevelFields, - @Nullable VariablePredicate variablePredicate) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); + @Nullable VariablePredicate variablePredicate, + @Nullable Map normalizedFieldToDeferExecution) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); } /** @@ -174,8 +175,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, - @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, true); + @Nullable VariablePredicate variablePredicate, + @Nullable Map normalizedFieldToDeferExecution) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); } private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -184,11 +186,11 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - boolean deferSupport) { + @Nullable Map normalizedFieldToDeferExecution) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); - List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport); + List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); SelectionSet selectionSet = new SelectionSet(selections); OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition() @@ -211,9 +213,9 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - boolean deferSupport) { - if (deferSupport) { - return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); + @Nullable Map normalizedFieldToDeferExecution) { + if (normalizedFieldToDeferExecution != null) { + return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); } else { return subselectionsForNormalizedFieldNoDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); } @@ -232,13 +234,13 @@ private static List> subselectionsForNormalizedFieldNoDeferSupport( for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, false) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false) .forEach((objectTypeName, field) -> fieldsByTypeCondition .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, false)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false)); } } @@ -259,6 +261,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, + @NotNull Map normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -268,31 +271,24 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { + DeferExecution deferExecution = normalizedFieldToDeferExecution.get(nf); + if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true) .forEach((objectTypeName, field) -> { - if (nf.getDeferExecution() == null) { - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) - .add(field); - } else { - nf.getDeferExecution().getLabels().stream() - .map(DeferLabel::getValue) - .forEach(label -> fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, new DeferLabel(label)), ignored -> new ArrayList<>()) - .add(field)); - } + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) + .add(field); }); - } else if (nf.getDeferExecution() != null) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true); - nf.getDeferExecution().getLabels().stream() - .map(DeferLabel::getValue) - .forEach(label -> fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(null, new DeferLabel(label)), ignored -> new ArrayList<>()) - .add(field)); + } else if (deferExecution != null) { + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true); + + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, deferExecution), ignored -> new ArrayList<>()) + .add(field); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true)); } } @@ -305,11 +301,11 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor fragmentBuilder.typeCondition(typeName); } - if (typeAndDeferPair.deferLabel != null) { + if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferLabel.getValue() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferLabel.getValue())).build()); + if (typeAndDeferPair.deferExecution.getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -329,11 +325,12 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, + @Nullable Map normalizedFieldToDeferExecution, boolean deferSupport) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, deferSupport)); } return groupedFields; @@ -347,6 +344,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, + @Nullable Map normalizedFieldToDeferExecution, boolean deferSupport) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { @@ -361,7 +359,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, executableNormalizedField.getChildren(), normalizedFieldToQueryDirectives, variableAccumulator, - deferSupport + normalizedFieldToDeferExecution ); } @@ -477,11 +475,11 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferLabel deferLabel; + private final DeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, DeferLabel deferLabel) { + public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { this.typeName = typeName; - this.deferLabel = deferLabel; + this.deferExecution = deferExecution; } @Override @@ -489,12 +487,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ExecutionFragmentDetails that = (ExecutionFragmentDetails) o; - return Objects.equals(typeName, that.typeName) && Objects.equals(deferLabel, that.deferLabel); + return Objects.equals(typeName, that.typeName) && Objects.equals(deferExecution, that.deferExecution); } @Override public int hashCode() { - return Objects.hash(typeName, deferLabel); + return Objects.hash(typeName, deferExecution); } } } diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 7754d6de83..f201be211b 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -2,39 +2,17 @@ import graphql.ExperimentalApi; -import java.util.LinkedHashSet; -import java.util.Set; +import javax.annotation.Nullable; -/** - * This class holds information about the defer execution of an ENF. - *

- * Given that an ENF can be linked with numerous defer labels, a {@link DeferExecution} instance comprises a - * collection of these labels. - *

- * For example, this query: - *

- *   query q {
- *    dog {
- *      ... @defer(label: "name-defer") {
- *        name
- *      }
- *      ... @defer(label: "another-name-defer") {
- *        name
- *      }
- *    }
- *  }
- *  
- * Will result on a ENF linked to a {@link DeferExecution} with both labels: "name-defer" and "another-name-defer" - */ @ExperimentalApi public class DeferExecution { - private final Set labels = new LinkedHashSet<>(); + private final String label; - public void addLabel(DeferLabel label) { - this.labels.add(label); + public DeferExecution(@Nullable String label) { + this.label = label; } - public Set getLabels() { - return this.labels; + public String getLabel() { + return label; } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 6f0bb219c4..4d09d5ca75 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -7,7 +7,6 @@ import graphql.execution.ValuesResolver; import graphql.language.Directive; import graphql.language.NodeUtil; -import graphql.schema.GraphQLSchema; import java.util.List; import java.util.Locale; @@ -30,13 +29,13 @@ public DeferLabel getDeferLabel( Object flag = argumentValues.get("if"); Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); - if(!((Boolean) flag)) { + if (!((Boolean) flag)) { return null; } Object label = argumentValues.get("label"); - if(label == null) { + if (label == null) { return new DeferLabel(null); } @@ -48,4 +47,35 @@ public DeferLabel getDeferLabel( return null; } + + public DeferExecution getDeferExecution( + Map variables, + List directives + ) { + Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); + + if (deferDirective != null) { + Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); + + Object flag = argumentValues.get("if"); + Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); + + if (!((Boolean) flag)) { + return null; + } + + Object label = argumentValues.get("label"); + + if (label == null) { + return new DeferExecution(null); + } + + Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); + + return new DeferExecution((String) label); + + } + + return null; + } } diff --git a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy index 1fa8f360e8..6460512543 100644 --- a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy +++ b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy @@ -21,7 +21,7 @@ class FieldCollectorTest extends Specification { type Query { bar1: String bar2: String - } + } """) def objectType = schema.getType("Query") as GraphQLObjectType FieldCollector fieldCollector = new FieldCollector() @@ -48,12 +48,12 @@ class FieldCollectorTest extends Specification { type Query{ bar1: String bar2: Test - } + } interface Test { - fieldOnInterface: String - } + fieldOnInterface: String + } type TestImpl implements Test { - fieldOnInterface: String + fieldOnInterface: String } """) def object = schema.getType("TestImpl") as GraphQLObjectType @@ -73,6 +73,136 @@ class FieldCollectorTest extends Specification { then: result.getSubField('fieldOnInterface').getFields() == [interfaceField] + } + + def "collect fields that are merged together - one of the fields is on an inline fragment "() { + def schema = TestUtil.schema(""" + type Query { + echo: String + } +""") + + Document document = new Parser().parseDocument(""" + { + echo + ... on Query { + echo + } + } + +""") + + def object = schema.getType("TestImpl") as GraphQLObjectType + FieldCollector fieldCollector = new FieldCollector() + FieldCollectorParameters fieldCollectorParameters = newParameters() + .schema(schema) + .objectType(object) + .build() + + def selectionSet = ((OperationDefinition) document.children[0]).selectionSet + + when: + def result = fieldCollector.collectFields(fieldCollectorParameters, selectionSet) + + then: + result.size() == 1 + result.getSubField('echo').fields.size() == 1 + } + + def "collect fields that are merged together - fields have different selection sets "() { + def schema = TestUtil.schema(""" + type Query { + me: Me + } + + type Me { + firstname: String + lastname: String + } +""") + + Document document = new Parser().parseDocument(""" + { + me { + firstname + } + me { + lastname + } + } + +""") + + def object = schema.getType("TestImpl") as GraphQLObjectType + FieldCollector fieldCollector = new FieldCollector() + FieldCollectorParameters fieldCollectorParameters = newParameters() + .schema(schema) + .objectType(object) + .build() + + def selectionSet = ((OperationDefinition) document.children[0]).selectionSet + + when: + def result = fieldCollector.collectFields(fieldCollectorParameters, selectionSet) + + then: + result.size() == 1 + + def meField = result.getSubField('me') + + meField.fields.size() == 2 + + meField.fields[0].selectionSet.selections.size() == 1 + meField.fields[0].selectionSet.selections[0].name == "firstname" + + meField.fields[1].selectionSet.selections.size() == 1 + meField.fields[1].selectionSet.selections[0].name == "lastname" + } + + def "collect fields that are merged together - fields have different directives"() { + def schema = TestUtil.schema(""" + directive @one on FIELD + directive @two on FIELD + + type Query { + echo: String + } +""") + + Document document = new Parser().parseDocument(""" + { + echo @one + echo @two + } + +""") + + def object = schema.getType("TestImpl") as GraphQLObjectType + FieldCollector fieldCollector = new FieldCollector() + FieldCollectorParameters fieldCollectorParameters = newParameters() + .schema(schema) + .objectType(object) + .build() + + def selectionSet = ((OperationDefinition) document.children[0]).selectionSet + + when: + def result = fieldCollector.collectFields(fieldCollectorParameters, selectionSet) + + then: + result.size() == 1 + + def echoField = result.getSubField('echo') + + echoField.fields.size() == 2 + + echoField.fields[0].name == "echo" + echoField.fields[0].directives.size() == 1 + echoField.fields[0].directives[0].name == "one" + + echoField.fields[1].name == "echo" + echoField.fields[1].directives.size() == 1 + echoField.fields[1].directives[0].name == "two" } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index fe8e98116a..ac7f58b9aa 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -74,7 +74,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -102,7 +102,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -133,7 +133,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -167,7 +167,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -198,7 +198,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -233,7 +233,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -270,7 +270,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -299,7 +299,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -347,7 +347,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -395,7 +395,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def fields = createNormalizedFields(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 05494950b4..ec88d85dfc 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2184,7 +2184,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat Map normalizedFieldToQueryDirectives, VariablePredicate variablePredicate) { if (deferSupport) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, [:]) } return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } From 8fe9e38793b2e76379930bd7ee7effb9e436092c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 20 Dec 2023 11:42:11 +1100 Subject: [PATCH 14/33] Conclude refactoring that moves defer support out of ENFs and into ENOs --- src/main/java/graphql/ExperimentalApi.java | 6 +- .../normalized/ExecutableNormalizedField.java | 28 --- .../ExecutableNormalizedOperation.java | 10 + .../ExecutableNormalizedOperationFactory.java | 212 ++++++++++-------- ...tableNormalizedOperationToAstCompiler.java | 58 +++-- .../incremental/DeferExecution.java | 20 +- .../normalized/incremental/DeferLabel.java | 39 ---- .../incremental/IncrementalNodes.java | 40 +--- ...NormalizedOperationFactoryDeferTest.groovy | 25 ++- ...tableNormalizedOperationFactoryTest.groovy | 33 +-- ...izedOperationToAstCompilerDeferTest.groovy | 171 ++++++++++---- ...ormalizedOperationToAstCompilerTest.groovy | 49 ++-- 12 files changed, 376 insertions(+), 315 deletions(-) delete mode 100644 src/main/java/graphql/normalized/incremental/DeferLabel.java diff --git a/src/main/java/graphql/ExperimentalApi.java b/src/main/java/graphql/ExperimentalApi.java index c405ec10cf..991932b2d4 100644 --- a/src/main/java/graphql/ExperimentalApi.java +++ b/src/main/java/graphql/ExperimentalApi.java @@ -12,9 +12,9 @@ /** * This represents code that the graphql-java project considers experimental API and while our intention is that it will - * progress to be {@link PublicApi}, its existence, signature of behavior may change between releases. - * - * In general unnecessary changes will be avoided but you should not depend on experimental classes being stable + * progress to be {@link PublicApi}, its existence, signature or behavior may change between releases. + *

+ * In general unnecessary changes will be avoided, but you should not depend on experimental classes being stable. */ @Retention(RetentionPolicy.RUNTIME) @Target(value = {CONSTRUCTOR, METHOD, TYPE, FIELD}) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 12ab96e87d..01f0d3fddd 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -3,14 +3,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import graphql.Assert; -import graphql.ExperimentalApi; import graphql.Internal; import graphql.Mutable; import graphql.PublicApi; import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -65,8 +63,6 @@ public class ExecutableNormalizedField { private final String fieldName; private final int level; - private final DeferExecution deferExecution; - private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; this.resolvedArguments = builder.resolvedArguments; @@ -77,7 +73,6 @@ private ExecutableNormalizedField(Builder builder) { this.children = builder.children; this.level = builder.level; this.parent = builder.parent; - this.deferExecution = builder.deferExecution; } /** @@ -363,19 +358,6 @@ public String getSingleObjectTypeName() { return objectTypeNames.iterator().next(); } - /** - * Returns an object containing the details about the defer aspect of this field's execution. - *

- * "null" is returned when this field is not supposed to be deferred. - * - * @return details about the defer execution - */ - @ExperimentalApi - @Nullable - public DeferExecution getDeferExecution() { - return deferExecution; - } - /** * @return a helper method show field details */ @@ -596,8 +578,6 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private DeferExecution deferExecution; - private Builder() { } @@ -606,7 +586,6 @@ private Builder(ExecutableNormalizedField existing) { this.normalizedArguments = existing.normalizedArguments; this.astArguments = existing.astArguments; this.resolvedArguments = existing.resolvedArguments; - this.deferExecution = existing.deferExecution; this.objectTypeNames = new LinkedHashSet<>(existing.getObjectTypeNames()); this.fieldName = existing.getFieldName(); this.children = new ArrayList<>(existing.children); @@ -667,13 +646,6 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - - @ExperimentalApi - public Builder deferExecution(DeferExecution deferExecution) { - this.deferExecution = deferExecution; - return this; - } - public ExecutableNormalizedField build() { return new ExecutableNormalizedField(this); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index 83cebdb77f..9bfef00b26 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableListMultimap; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.MergedField; import graphql.execution.ResultPath; @@ -130,6 +131,15 @@ public Map getNormalizedFieldToQuery } + /** + * @return a map of {@link ExecutableNormalizedField} to its {@link DeferExecution} + */ + @ExperimentalApi + public ImmutableListMultimap getNormalizedFieldToDeferExecution() { + return normalizedFieldToDeferExecution; + } + + /** * This looks up the {@link QueryDirectives} associated with the given {@link ExecutableNormalizedField} * diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 081146202b..33682b176b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import graphql.Assert; import graphql.ExperimentalApi; import graphql.GraphQLContext; import graphql.PublicApi; @@ -49,8 +50,12 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; import static graphql.Assert.assertShouldNeverHappen; @@ -61,6 +66,7 @@ import static graphql.util.FpKit.intersection; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toSet; /** * This factory can create a {@link ExecutableNormalizedOperation} which represents what would be executed @@ -73,19 +79,24 @@ public static class Options { private final Locale locale; private final int maxChildrenDepth; + private final boolean deferSupport; + private Options(GraphQLContext graphQLContext, Locale locale, - int maxChildrenDepth) { + int maxChildrenDepth, + boolean deferSupport) { this.graphQLContext = graphQLContext; this.locale = locale; this.maxChildrenDepth = maxChildrenDepth; + this.deferSupport = deferSupport; } public static Options defaultOptions() { return new Options( GraphQLContext.getDefault(), Locale.getDefault(), - Integer.MAX_VALUE); + Integer.MAX_VALUE, + false); } /** @@ -97,7 +108,7 @@ public static Options defaultOptions() { * @return new options object to use */ public Options locale(Locale locale) { - return new Options(this.graphQLContext, locale, this.maxChildrenDepth); + return new Options(this.graphQLContext, locale, this.maxChildrenDepth, true); } /** @@ -109,7 +120,7 @@ public Options locale(Locale locale) { * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { - return new Options(graphQLContext, this.locale, this.maxChildrenDepth); + return new Options(graphQLContext, this.locale, this.maxChildrenDepth, true); } /** @@ -120,7 +131,18 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { - return new Options(this.graphQLContext, this.locale, maxChildrenDepth); + return new Options(this.graphQLContext, this.locale, maxChildrenDepth, true); + } + + /** + * Controls whether defer execution is supported when creating instances of {@link ExecutableNormalizedOperation}. + * + * @param deferSupport true to enable support for defer + * @return new options object to use + */ + @ExperimentalApi + public Options deferSupport(boolean deferSupport) { + return new Options(this.graphQLContext, this.locale, this.maxChildrenDepth, deferSupport); } /** @@ -146,6 +168,12 @@ public Locale getLocale() { public int getMaxChildrenDepth() { return maxChildrenDepth; } + + // TODO: Javadoc + @ExperimentalApi + public boolean getDeferSupport() { + return deferSupport; + } } private final ConditionalNodes conditionalNodes = new ConditionalNodes(); @@ -166,6 +194,32 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( Document document, String operationName, CoercedVariables coercedVariableValues + ) { + return createExecutableNormalizedOperation( + graphQLSchema, + document, + operationName, + coercedVariableValues, + Options.defaultOptions()); + } + + /** + * This will create a runtime representation of the graphql operation that would be executed + * in a runtime sense. + * + * @param graphQLSchema the schema to be used + * @param document the {@link Document} holding the operation text + * @param operationName the operation name to use + * @param coercedVariableValues the coerced variables to use + * @param options the {@link Options} to use for parsing + * @return a runtime representation of the graphql operation. + */ + public static ExecutableNormalizedOperation createExecutableNormalizedOperation( + GraphQLSchema graphQLSchema, + Document document, + String operationName, + CoercedVariables coercedVariableValues, + Options options ) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, @@ -173,8 +227,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( getOperationResult.fragmentsByName, coercedVariableValues, null, - Options.defaultOptions(), - false); + options); } /** @@ -196,8 +249,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( fragments, coercedVariableValues, null, - Options.defaultOptions(), - false); + Options.defaultOptions()); } /** @@ -273,72 +325,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW getOperationResult.operationDefinition, getOperationResult.fragmentsByName, rawVariables, - options, - false - ); - } - - - /** - * This will create a runtime representation of the graphql operation that would be executed - * in a runtime sense. - *

- * This version of the "createExecutableNormalizedOperation" method has support for the `@defer` directive, - * which is still a experimental feature in GraphQL Java. - * - * @param graphQLSchema the schema to be used - * @param document the {@link Document} holding the operation text - * @param operationName the operation name to use - * @param coercedVariableValues the coerced variables to use - * @return a runtime representation of the graphql operation. - * @see ExecutableNormalizedOperationFactory#createExecutableNormalizedOperation(GraphQLSchema, Document, String, CoercedVariables) - */ - @ExperimentalApi - public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithDeferSupport( - GraphQLSchema graphQLSchema, - Document document, - String operationName, - CoercedVariables coercedVariableValues - ) { - NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - return new ExecutableNormalizedOperationFactory().createNormalizedQueryImpl(graphQLSchema, - getOperationResult.operationDefinition, - getOperationResult.fragmentsByName, - coercedVariableValues, - null, - Options.defaultOptions(), - true); - } - - /** - * This will create a runtime representation of the graphql operation that would be executed - * in a runtime sense. - * - *

- * This version of the "createExecutableNormalizedOperationWithRawVariables" method has support for the `@defer` - * directive, which is still a experimental feature in GraphQL Java. - * - * @param graphQLSchema the schema to be used - * @param document the {@link Document} holding the operation text - * @param operationName the operation name to use - * @param rawVariables the raw variables that have not yet been coerced - * @param options the {@link Options} to use for parsing - * @return a runtime representation of the graphql operation. - */ - @ExperimentalApi - public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariablesWithDeferSupport(GraphQLSchema graphQLSchema, - Document document, - String operationName, - RawVariables rawVariables, - Options options) { - NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - - return new ExecutableNormalizedOperationFactory().createExecutableNormalizedOperationImplWithRawVariables(graphQLSchema, - getOperationResult.operationDefinition, - getOperationResult.fragmentsByName, - rawVariables, - options, - true + options ); } @@ -346,8 +333,7 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit OperationDefinition operationDefinition, Map fragments, RawVariables rawVariables, - Options options, - boolean deferSupport) { + Options options) { List variableDefinitions = operationDefinition.getVariableDefinitions(); CoercedVariables coercedVariableValues = ValuesResolver.coerceVariableValues(graphQLSchema, variableDefinitions, @@ -364,8 +350,7 @@ private ExecutableNormalizedOperation createExecutableNormalizedOperationImplWit fragments, coercedVariableValues, normalizedVariableValues, - options, - deferSupport); + options); } /** @@ -376,8 +361,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr Map fragments, CoercedVariables coercedVariableValues, @Nullable Map normalizedVariableValues, - Options options, - boolean deferSupport) { + Options options) { FieldCollectorNormalizedQueryParams parameters = FieldCollectorNormalizedQueryParams .newParameters() .fragments(fragments) @@ -388,7 +372,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr GraphQLObjectType rootType = SchemaUtil.getOperationRootType(graphQLSchema, operationDefinition); - CollectNFResult collectFromOperationResult = collectFromOperation(parameters, operationDefinition, rootType); + CollectNFResult collectFromOperationResult = collectFromOperation(parameters, operationDefinition, rootType, options.getDeferSupport()); ImmutableListMultimap.Builder fieldToNormalizedField = ImmutableListMultimap.builder(); ImmutableMap.Builder normalizedFieldToMergedField = ImmutableMap.builder(); @@ -402,6 +386,13 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; + ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); + + Consumer captureCollectNFResult = (collectNFResult -> { + normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); + }); + for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); MergedField mergedField = newMergedField(fieldAndAstParents); @@ -420,7 +411,8 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), - deferSupport); + captureCollectNFResult, + options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); @@ -434,7 +426,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), coordinatesToNormalizedFields.build(), - collectFromOperationResult.normalizedFieldToDeferExecution + normalizedFieldToDeferExecution.build() ); } @@ -447,6 +439,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, + Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -454,6 +447,8 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); + captureCollectNFResult.accept(nextLevel); + for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); @@ -472,6 +467,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, + captureCollectNFResult, deferSupport); } } @@ -565,9 +561,8 @@ private Map> fieldsByResultKey(List public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, OperationDefinition operationDefinition, - GraphQLObjectType rootType) { - - + GraphQLObjectType rootType, + boolean deferSupport) { Set possibleObjects = ImmutableSet.of(rootType); List collectedFields = new ArrayList<>(); collectFromSelectionSet(parameters, operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects, null); @@ -577,11 +572,17 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, false); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } + public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, + OperationDefinition operationDefinition, + GraphQLObjectType rootType) { + return this.collectFromOperation(parameters, operationDefinition, rootType, false); + } + private void createNFs(ImmutableList.Builder nfListBuilder, FieldCollectorNormalizedQueryParams parameters, Map> fieldsByName, @@ -602,6 +603,9 @@ private void createNFs(ImmutableList.Builder nfListBu normalizedFieldToAstFields.put(nf, new FieldAndAstParent(collectedField.field, collectedField.astTypeCondition)); } nfListBuilder.add(nf); + if (deferSupport) { + normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + } } if (commonParentsGroups.size() > 1) { parameters.addPossibleMergers(parent, resultKey); @@ -693,6 +697,14 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set allRelevantObjects = objectTypes.build(); Set deferExecutions = deferExecutionsBuilder.build(); + + Set duplicatedLabels = listDuplicatedLabels(deferExecutions); + + if (!duplicatedLabels.isEmpty()) { + // Query validation should pick this up + Assert.assertShouldNeverHappen("Duplicated @defer labels are not allowed: [%s]", String.join(",", duplicatedLabels)); + } + Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecutions)); @@ -705,6 +717,17 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } + private Set listDuplicatedLabels(Collection deferExecutions) { + return deferExecutions.stream() + .map(DeferExecution::getLabel) + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .collect(toSet()); + } private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams parameters, SelectionSet selectionSet, @@ -766,7 +789,11 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), fragmentSpread.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + parameters.getCoercedVariableValues(), + fragmentSpread.getDirectives(), + fragmentDefinition.getTypeCondition() + ); GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); @@ -789,10 +816,13 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter if (inlineFragment.getTypeCondition() != null) { newAstTypeCondition = (GraphQLCompositeType) parameters.getGraphQLSchema().getType(inlineFragment.getTypeCondition().getName()); newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution(parameters.getCoercedVariableValues(), inlineFragment.getDirectives()); + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + parameters.getCoercedVariableValues(), + inlineFragment.getDirectives(), + inlineFragment.getTypeCondition() + ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 1737eb9dec..156c31e3d6 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -1,6 +1,7 @@ package graphql.normalized; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import graphql.Assert; import graphql.Directives; @@ -150,7 +151,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @Nullable VariablePredicate variablePredicate, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); } @@ -176,7 +177,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); } @@ -186,7 +187,7 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); @@ -213,7 +214,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map normalizedFieldToDeferExecution) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { if (normalizedFieldToDeferExecution != null) { return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); } else { @@ -234,13 +235,13 @@ private static List> subselectionsForNormalizedFieldNoDeferSupport( for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null) .forEach((objectTypeName, field) -> fieldsByTypeCondition .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null, false)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null)); } } @@ -261,7 +262,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - @NotNull Map normalizedFieldToDeferExecution, + @NotNull ImmutableListMultimap normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -271,24 +272,37 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - DeferExecution deferExecution = normalizedFieldToDeferExecution.get(nf); + List deferExecutions = normalizedFieldToDeferExecution.get(nf); if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution) .forEach((objectTypeName, field) -> { - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) - .add(field); + if (deferExecutions.isEmpty()) { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) + .add(field); + } else { + deferExecutions.forEach(deferExecution -> { + if (deferExecution.getTargetType() == null || + objectTypeName.equals(deferExecution.getTargetType().getName())) { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) + .add(field); + } + }); + } }); - } else if (deferExecution != null) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true); + } else if (!deferExecutions.isEmpty()) { + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(null, deferExecution), ignored -> new ArrayList<>()) - .add(field); + deferExecutions.forEach(deferExecution -> { + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(null, deferExecution), ignored -> new ArrayList<>()) + .add(field); + }); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, true)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); } } @@ -325,12 +339,11 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map normalizedFieldToDeferExecution, - boolean deferSupport) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution, deferSupport)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); } return groupedFields; @@ -344,8 +357,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map normalizedFieldToDeferExecution, - boolean deferSupport) { + @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index f201be211b..8d62c0acaa 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -1,18 +1,36 @@ package graphql.normalized.incremental; import graphql.ExperimentalApi; +import graphql.language.TypeName; import javax.annotation.Nullable; +/** + * Represents the defer execution aspect of a query field + */ @ExperimentalApi public class DeferExecution { private final String label; + private final TypeName targetType; - public DeferExecution(@Nullable String label) { + public DeferExecution(@Nullable String label, @Nullable TypeName targetType) { this.label = label; + this.targetType = targetType; } + /** + * @return the label associated with this defer execution + */ + @Nullable public String getLabel() { return label; } + + /** + * @return the {@link TypeName} of the type that is the target of the defer execution + */ + @Nullable + public TypeName getTargetType() { + return targetType; + } } diff --git a/src/main/java/graphql/normalized/incremental/DeferLabel.java b/src/main/java/graphql/normalized/incremental/DeferLabel.java deleted file mode 100644 index 4c6249bd62..0000000000 --- a/src/main/java/graphql/normalized/incremental/DeferLabel.java +++ /dev/null @@ -1,39 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.ExperimentalApi; - -import javax.annotation.Nullable; -import java.util.Objects; - -/** - * Holds the value of the 'label' argument of a defer directive. - *

- * 'value' can be 'null', as 'label' is not a required argument on the '@defer' directive. - */ -@ExperimentalApi -public class DeferLabel { - private final String value; - - public DeferLabel(@Nullable String value) { - this.value = value; - } - - @Nullable - public String getValue() { - return value; - } - - @Override - public int hashCode() { - return Objects.hashCode(this.value); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof DeferLabel) { - return Objects.equals(this.value, ((DeferLabel) obj).value); - } - - return false; - } -} diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 4d09d5ca75..9898616ddc 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -7,7 +7,9 @@ import graphql.execution.ValuesResolver; import graphql.language.Directive; import graphql.language.NodeUtil; +import graphql.language.TypeName; +import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; @@ -17,40 +19,10 @@ @Internal public class IncrementalNodes { - public DeferLabel getDeferLabel( - Map variables, - List directives - ) { - Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); - - if (deferDirective != null) { - Map argumentValues = ValuesResolver.getArgumentValues(DeferDirective.getArguments(), deferDirective.getArguments(), CoercedVariables.of(variables), GraphQLContext.getDefault(), Locale.getDefault()); - - Object flag = argumentValues.get("if"); - Assert.assertTrue(flag instanceof Boolean, () -> String.format("The '%s' directive MUST have a value for the 'if' argument", DeferDirective.getName())); - - if (!((Boolean) flag)) { - return null; - } - - Object label = argumentValues.get("label"); - - if (label == null) { - return new DeferLabel(null); - } - - Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - - return new DeferLabel((String) label); - - } - - return null; - } - public DeferExecution getDeferExecution( Map variables, - List directives + List directives, + @Nullable TypeName targetType ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -67,12 +39,12 @@ public DeferExecution getDeferExecution( Object label = argumentValues.get("label"); if (label == null) { - return new DeferExecution(null); + return new DeferExecution(null, targetType); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label); + return new DeferExecution((String) label, targetType); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 3d5f809fa1..9c184e5966 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -1,6 +1,6 @@ package graphql.normalized - +import graphql.AssertException import graphql.ExecutionInput import graphql.GraphQL import graphql.TestUtil @@ -233,7 +233,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "multiple fields and a multiple defers with same label"() { + def "multiple fields and a multiple defers with same label are not allowed"() { given: String query = ''' @@ -254,12 +254,11 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Map variables = [:] when: - List printedTree = executeQueryAndPrintTree(query, variables) + executeQueryAndPrintTree(query, variables) then: - printedTree == ['Query.dog', - 'Dog.name defer[name-defer]', - ] + def exception = thrown(AssertException) + exception.message == "Internal error: should never happen: Duplicated @defer labels are not allowed: [name-defer]" } def "nested defers - no label"() { @@ -482,12 +481,12 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Document document = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + def tree = dependencyGraph.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, null, RawVariables.of(variables), - ExecutableNormalizedOperationFactory.Options.defaultOptions(), + ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true), ) return printTreeWithIncrementalExecutionDetails(tree) } @@ -495,6 +494,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private List printTreeWithIncrementalExecutionDetails(ExecutableNormalizedOperation queryExecutionTree) { def result = [] Traverser traverser = Traverser.depthFirst({ it.getChildren() }) + + def normalizedFieldToDeferExecution = queryExecutionTree.normalizedFieldToDeferExecution + traverser.traverse(queryExecutionTree.getTopLevelFields(), new TraverserVisitorStub() { @Override TraversalControl enter(TraverserContext context) { @@ -504,12 +506,13 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } String printDeferExecutionDetails(ExecutableNormalizedField field) { - if (field.getDeferExecution() == null) { + def deferExecutions = normalizedFieldToDeferExecution.get(field) + if (deferExecutions.isEmpty()) { return "" } - def deferLabels = field.getDeferExecution().labels - .collect { it.value } + def deferLabels = deferExecutions + .collect { it.label } .join(",") return " defer[" + deferLabels + "]" diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 72fa48b247..432075a363 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -2885,11 +2885,9 @@ fragment personName on Person { CoercedVariables coercedVariableValues ) { ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - if (deferSupport) { - return dependencyGraph.createExecutableNormalizedOperationWithDeferSupport(graphQLSchema, document, operationName, coercedVariableValues) - } else { - return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues) - } + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) + + return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) } private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( @@ -2899,22 +2897,15 @@ fragment personName on Person { RawVariables rawVariables ) { ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - if (deferSupport) { - return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( - graphQLSchema, - document, - operationName, - rawVariables, - ExecutableNormalizedOperationFactory.Options.defaultOptions() - ) - } else { - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( - graphQLSchema, - document, - operationName, - rawVariables - ) - } + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) + + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + operationName, + rawVariables, + options + ) } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index ac7f58b9aa..c334620a17 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -1,5 +1,6 @@ package graphql.normalized + import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables @@ -72,9 +73,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -100,9 +101,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -131,9 +132,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -150,30 +151,33 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } - def "multiple defers with same label on the same field"() { + def "multiple defers without label on the same field"() { String query = """ query q { dog { name - ... @defer(label: "breed-defer") { + ... @defer { breed } - ... @defer(label: "breed-defer") { + ... @defer { breed } } } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ dog { name - ... @defer(label: "breed-defer") { + ... @defer { + breed + } + ... @defer { breed } } @@ -181,29 +185,27 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } - def "multiple defers without label on the same field"() { + def "field with and without defer"() { String query = """ query q { dog { - name ... @defer { breed } - ... @defer { + ... { breed } } } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ dog { - name ... @defer { breed } @@ -231,9 +233,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -268,9 +270,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -278,6 +280,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { name } + ... @defer { + name + } } } ''' @@ -297,9 +302,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -315,6 +320,76 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "2 fragments on conditional fields with different labels"() { + String query = """ + query q { + animal { + ... on Cat @defer(label: "cat-defer") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def tree = createNormalizedTree(schema, query) + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... on Cat @defer(label: "cat-defer") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } +} +''' + } + + def "fragments on conditional fields with different labels and repeating types"() { + String query = """ + query q { + animal { + ... on Cat @defer(label: "cat-defer-1") { + breed + } + ... on Cat @defer(label: "cat-defer-2") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def tree = createNormalizedTree(schema, query) + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + animal { + ... on Cat @defer(label: "cat-defer-1") { + breed + } + ... on Cat @defer(label: "cat-defer-2") { + breed + } + ... on Dog @defer(label: "dog-defer") { + breed + } + } +} +''' + } + def "nested defer"() { String query = """ query q { @@ -345,19 +420,25 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ animal { + ... @defer { + name + } ... @defer { name } ... on Dog @defer { owner { firstname + ... @defer { + lastname + } ... @defer { bestFriend { firstname @@ -365,7 +446,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification lastname } } - lastname } } } @@ -393,26 +473,26 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification } """ GraphQLSchema schema = mkSchema(sdl) - def fields = createNormalizedFields(schema, query) + def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, fields, noVariables, [:]) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ - dog { - ... @defer { - name - } - ... @defer { - breed - } - ... @defer { - owner { - firstname - } - } + dog { + ... @defer { + name + } + ... @defer { + breed + } + ... @defer { + owner { + firstname + } } } +} ''' } @@ -421,19 +501,16 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification Document originalDocument = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariablesWithDeferSupport( + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true) + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( schema, originalDocument, null, RawVariables.of(variables), - ExecutableNormalizedOperationFactory.Options.defaultOptions() + options ) } - private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { - return createNormalizedTree(schema, query, variables).getTopLevelFields() - } - private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() assert graphQL.execute(newExecutionInput().query(query).variables(variables)).errors.isEmpty() diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index ec88d85dfc..e0720dca3c 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -1,5 +1,6 @@ package graphql.normalized +import com.google.common.collect.ImmutableListMultimap import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables @@ -11,6 +12,7 @@ import graphql.language.Field import graphql.language.IntValue import graphql.language.OperationDefinition import graphql.language.StringValue +import graphql.normalized.incremental.DeferExecution import graphql.parser.Parser import graphql.schema.GraphQLSchema import graphql.schema.idl.RuntimeWiring @@ -201,10 +203,9 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat """ def tree = createNormalizedTree(schema, query) - // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -255,7 +256,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -336,7 +337,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -428,7 +429,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -523,7 +524,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -590,7 +591,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -647,7 +648,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -705,7 +706,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -772,7 +773,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -870,7 +871,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -968,7 +969,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1284,7 +1285,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat when: - def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables, null) OperationDefinition operationDefinition = result.document.getDefinitionsOfType(OperationDefinition.class)[0] def fooField = (Field) operationDefinition.selectionSet.children[0] def nameField = (Field) fooField.selectionSet.children[0] @@ -2147,7 +2148,8 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat Document originalDocument = TestUtil.parseQuery(query) ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables)) + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables(schema, originalDocument, null, RawVariables.of(variables), options) } private List createNormalizedFields(GraphQLSchema schema, String query, Map variables = [:]) { @@ -2173,7 +2175,18 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat List topLevelFields, VariablePredicate variablePredicate ) { - return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate) + return localCompileToDocument(schema, operationKind, operationName, topLevelFields, variablePredicate, null) + } + + private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( + GraphQLSchema schema, + OperationDefinition.Operation operationKind, + String operationName, + List topLevelFields, + VariablePredicate variablePredicate, + ImmutableListMultimap normalizedFieldToDeferExecution + ) { + return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution) } private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( @@ -2182,9 +2195,11 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat String operationName, List topLevelFields, Map normalizedFieldToQueryDirectives, - VariablePredicate variablePredicate) { + VariablePredicate variablePredicate, + ImmutableListMultimap normalizedFieldToDeferExecution + ) { if (deferSupport) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, [:]) + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution) } return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } From da2761e8355b082046c09c6c1bd1b0d22022d195 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 20 Dec 2023 11:50:31 +1100 Subject: [PATCH 15/33] Clean up defer execution code --- .../java/graphql/DeferredExecutionResult.java | 16 ---- .../graphql/DeferredExecutionResultImpl.java | 68 --------------- .../graphql/execution/defer/DeferSupport.java | 82 ------------------- .../graphql/execution/defer/DeferredCall.java | 43 ---------- .../execution/defer/DeferredErrorSupport.java | 31 ------- .../DeferredFieldInstrumentationContext.java | 12 --- ...nstrumentationDeferredFieldParameters.java | 25 ------ 7 files changed, 277 deletions(-) delete mode 100644 src/main/java/graphql/DeferredExecutionResult.java delete mode 100644 src/main/java/graphql/DeferredExecutionResultImpl.java delete mode 100644 src/main/java/graphql/execution/defer/DeferSupport.java delete mode 100644 src/main/java/graphql/execution/defer/DeferredCall.java delete mode 100644 src/main/java/graphql/execution/defer/DeferredErrorSupport.java delete mode 100644 src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java delete mode 100644 src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java diff --git a/src/main/java/graphql/DeferredExecutionResult.java b/src/main/java/graphql/DeferredExecutionResult.java deleted file mode 100644 index 18eb23c74a..0000000000 --- a/src/main/java/graphql/DeferredExecutionResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package graphql; - -import java.util.List; - -/** - * Results that come back from @defer fields have an extra path property that tells you where - * that deferred result came in the original query - */ -@PublicApi -public interface DeferredExecutionResult extends ExecutionResult { - - /** - * @return the execution path of this deferred result in the original query - */ - List getPath(); -} diff --git a/src/main/java/graphql/DeferredExecutionResultImpl.java b/src/main/java/graphql/DeferredExecutionResultImpl.java deleted file mode 100644 index b5a57eadd4..0000000000 --- a/src/main/java/graphql/DeferredExecutionResultImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package graphql; - -import graphql.execution.ResultPath; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static graphql.Assert.assertNotNull; - -/** - * Results that come back from @defer fields have an extra path property that tells you where - * that deferred result came in the original query - */ -@PublicApi -public class DeferredExecutionResultImpl extends ExecutionResultImpl implements DeferredExecutionResult { - - private final List path; - - private DeferredExecutionResultImpl(List path, ExecutionResultImpl executionResult) { - super(executionResult); - this.path = assertNotNull(path); - } - - /** - * @return the execution path of this deferred result in the original query - */ - public List getPath() { - return path; - } - - @Override - public Map toSpecification() { - Map map = new LinkedHashMap<>(super.toSpecification()); - map.put("path", path); - return map; - } - - public static Builder newDeferredExecutionResult() { - return new Builder(); - } - - public static class Builder { - private List path = Collections.emptyList(); - private ExecutionResultImpl.Builder builder = ExecutionResultImpl.newExecutionResult(); - - public Builder path(ResultPath path) { - this.path = assertNotNull(path).toList(); - return this; - } - - public Builder from(ExecutionResult executionResult) { - builder.from(executionResult); - return this; - } - - public Builder addErrors(List errors) { - builder.addErrors(errors); - return this; - } - - public DeferredExecutionResult build() { - ExecutionResultImpl build = (ExecutionResultImpl) builder.build(); - return new DeferredExecutionResultImpl(path, build); - } - } -} diff --git a/src/main/java/graphql/execution/defer/DeferSupport.java b/src/main/java/graphql/execution/defer/DeferSupport.java deleted file mode 100644 index 3bc777b8c5..0000000000 --- a/src/main/java/graphql/execution/defer/DeferSupport.java +++ /dev/null @@ -1,82 +0,0 @@ -package graphql.execution.defer; - -import graphql.DeferredExecutionResult; -import graphql.Directives; -import graphql.ExecutionResult; -import graphql.Internal; -import graphql.execution.MergedField; -import graphql.execution.ValuesResolver; -import graphql.execution.reactive.SingleSubscriberPublisher; -import graphql.language.Directive; -import graphql.language.Field; -import org.reactivestreams.Publisher; - -import java.util.Deque; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.atomic.AtomicBoolean; - -import static graphql.Directives.*; - -/** - * This provides support for @defer directives on fields that mean that results will be sent AFTER - * the main result is sent via a Publisher stream. - */ -@Internal -public class DeferSupport { - - private final AtomicBoolean deferDetected = new AtomicBoolean(false); - private final Deque deferredCalls = new ConcurrentLinkedDeque<>(); - private final SingleSubscriberPublisher publisher = new SingleSubscriberPublisher<>(); -// private final ValuesResolver valuesResolver = new ValuesResolver(); - -// public boolean checkForDeferDirective(MergedField currentField, Map variables) { -// for (Field field : currentField.getFields()) { -// Directive directive = field.getDirective(DeferDirective.getName()); -// if (directive != null) { -// Map argumentValues = valuesResolver.getArgumentValues(DeferDirective.getArguments(), directive.getArguments(), variables); -// return (Boolean) argumentValues.get("if"); -// } -// } -// return false; -// } -// -// @SuppressWarnings("FutureReturnValueIgnored") -// private void drainDeferredCalls() { -// if (deferredCalls.isEmpty()) { -// publisher.noMoreData(); -// return; -// } -// DeferredCall deferredCall = deferredCalls.pop(); -// CompletableFuture future = deferredCall.invoke(); -// future.whenComplete((executionResult, exception) -> { -// if (exception != null) { -// publisher.offerError(exception); -// return; -// } -// publisher.offer(executionResult); -// drainDeferredCalls(); -// }); -// } -// -// public void enqueue(DeferredCall deferredCall) { -// deferDetected.set(true); -// deferredCalls.offer(deferredCall); -// } -// -// public boolean isDeferDetected() { -// return deferDetected.get(); -// } -// -// /** -// * When this is called the deferred execution will begin -// * -// * @return the publisher of deferred results -// */ -// public Publisher startDeferredCalls() { -// drainDeferredCalls(); -// return publisher; -// } -} diff --git a/src/main/java/graphql/execution/defer/DeferredCall.java b/src/main/java/graphql/execution/defer/DeferredCall.java deleted file mode 100644 index b52e124d5b..0000000000 --- a/src/main/java/graphql/execution/defer/DeferredCall.java +++ /dev/null @@ -1,43 +0,0 @@ -package graphql.execution.defer; - -import graphql.DeferredExecutionResult; -import graphql.DeferredExecutionResultImpl; -import graphql.ExecutionResult; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.ResultPath; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; - -/** - * This represents a deferred call (aka @defer) to get an execution result sometime after - * the initial query has returned - */ -@Internal -public class DeferredCall { - private final ResultPath path; - private final Supplier> call; - private final DeferredErrorSupport errorSupport; - - public DeferredCall(ResultPath path, Supplier> call, DeferredErrorSupport deferredErrorSupport) { - this.path = path; - this.call = call; - this.errorSupport = deferredErrorSupport; - } - - CompletableFuture invoke() { - CompletableFuture future = call.get(); - return future.thenApply(this::transformToDeferredResult); - } - - private DeferredExecutionResult transformToDeferredResult(ExecutionResult executionResult) { - List errorsEncountered = errorSupport.getErrors(); - DeferredExecutionResultImpl.Builder builder = DeferredExecutionResultImpl.newDeferredExecutionResult().from(executionResult); - return builder - .addErrors(errorsEncountered) - .path(path) - .build(); - } -} diff --git a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java b/src/main/java/graphql/execution/defer/DeferredErrorSupport.java deleted file mode 100644 index 3ef4f34c5d..0000000000 --- a/src/main/java/graphql/execution/defer/DeferredErrorSupport.java +++ /dev/null @@ -1,31 +0,0 @@ -package graphql.execution.defer; - -import graphql.ExceptionWhileDataFetching; -import graphql.GraphQLError; -import graphql.Internal; -import graphql.execution.ExecutionStrategyParameters; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * This captures errors that occur while a deferred call is being made - */ -@Internal -public class DeferredErrorSupport { - - private final List errors = new CopyOnWriteArrayList<>(); - - public void onFetchingException(ExecutionStrategyParameters parameters, Throwable e) { - ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(parameters.getPath(), e, parameters.getField().getSingleField().getSourceLocation()); - onError(error); - } - - public void onError(GraphQLError gError) { - errors.add(gError); - } - - public List getErrors() { - return errors; - } -} diff --git a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java b/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java deleted file mode 100644 index 39de62be19..0000000000 --- a/src/main/java/graphql/execution/instrumentation/DeferredFieldInstrumentationContext.java +++ /dev/null @@ -1,12 +0,0 @@ -package graphql.execution.instrumentation; - -import graphql.ExecutionResult; -import graphql.execution.FieldValueInfo; - -public interface DeferredFieldInstrumentationContext extends InstrumentationContext { - - default void onFieldValueInfo(FieldValueInfo fieldValueInfo) { - - } - -} diff --git a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java b/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java deleted file mode 100644 index e71d104438..0000000000 --- a/src/main/java/graphql/execution/instrumentation/parameters/InstrumentationDeferredFieldParameters.java +++ /dev/null @@ -1,25 +0,0 @@ -package graphql.execution.instrumentation.parameters; - -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; -import graphql.execution.ExecutionStrategyParameters; - -import java.util.function.Supplier; - -/** - * Parameters sent to {@link graphql.execution.instrumentation.Instrumentation} methods - */ -public class InstrumentationDeferredFieldParameters extends InstrumentationFieldParameters { - - private final ExecutionStrategyParameters executionStrategyParameters; - - - public InstrumentationDeferredFieldParameters(ExecutionContext executionContext, Supplier executionStepInfo, ExecutionStrategyParameters executionStrategyParameters) { - super(executionContext, executionStepInfo); - this.executionStrategyParameters = executionStrategyParameters; - } - - public ExecutionStrategyParameters getExecutionStrategyParameters() { - return executionStrategyParameters; - } -} From 3f6e480e4ac5eae4fd52ff926fc329b392cee95c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 20 Dec 2023 11:53:01 +1100 Subject: [PATCH 16/33] Add missing Javadoc --- .../normalized/ExecutableNormalizedOperationFactory.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 33682b176b..c70e5de517 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -169,7 +169,10 @@ public int getMaxChildrenDepth() { return maxChildrenDepth; } - // TODO: Javadoc + /** + * @return whether support for defer is enabled + * @see #deferSupport(boolean) + */ @ExperimentalApi public boolean getDeferSupport() { return deferSupport; From 7f0a37c1111c6d8fdce6b5ecc2775dd40d34daa7 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Thu, 21 Dec 2023 09:28:41 +1100 Subject: [PATCH 17/33] Add Javadocs and one more test --- ...tableNormalizedOperationToAstCompiler.java | 12 +++++---- ...izedOperationToAstCompilerDeferTest.groovy | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 156c31e3d6..6d35061965 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -137,11 +137,12 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * - * @param schema the graphql schema to use - * @param operationKind the kind of operation - * @param operationName the name of the operation to use - * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from - * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work * @return a {@link CompilerResult} object * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, VariablePredicate) */ @@ -167,6 +168,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work * @return a {@link CompilerResult} object * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, Map, VariablePredicate) */ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index c334620a17..d2c88489b6 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -89,6 +89,33 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } + def "@defer directives are not generated when map is null"() { + String query = """ + query q { + dog { + name + ... @defer(label: "breed-defer") { + breed + } + } + } + """ + GraphQLSchema schema = mkSchema(sdl) + def tree = createNormalizedTree(schema, query) + def normalizedFieldToDeferExecution = null + when: + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, normalizedFieldToDeferExecution) + def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) + then: + printed == '''{ + dog { + breed + name + } +} +''' + } + def "simple defer with named spread"() { String query = """ query q { From e979b771266c312e1fa07186a96244d92fc93b4b Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 2 Jan 2024 14:53:44 +1100 Subject: [PATCH 18/33] Apply code formatting --- .../normalized/ExecutableNormalizedField.java | 9 +++++++++ .../ExecutableNormalizedOperationFactory.java | 14 ++++++++++++++ ...ExecutableNormalizedOperationToAstCompiler.java | 14 ++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 01f0d3fddd..78b4c6d8e4 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -128,6 +128,7 @@ private ExecutableNormalizedField(Builder builder) { * NOT {@code Cat} or {@code Dog} as their respective implementations would say. * * @param schema - the graphql schema in play + * * @return true if the field is conditional */ public boolean isConditional(@NotNull GraphQLSchema schema) { @@ -260,6 +261,7 @@ public void clearChildren() { * WARNING: This is not always the key in the execution result, because of possible field aliases. * * @return the name of this {@link ExecutableNormalizedField} + * * @see #getResultKey() * @see #getAlias() */ @@ -269,6 +271,7 @@ public String getName() { /** * @return the same value as {@link #getName()} + * * @see #getResultKey() * @see #getAlias() */ @@ -281,6 +284,7 @@ public String getFieldName() { * This is either a field alias or the value of {@link #getName()} * * @return the result key for this {@link ExecutableNormalizedField}. + * * @see #getName() */ public String getResultKey() { @@ -292,6 +296,7 @@ public String getResultKey() { /** * @return the field alias used or null if there is none + * * @see #getResultKey() * @see #getName() */ @@ -310,6 +315,7 @@ public ImmutableList getAstArguments() { * Returns an argument value as a {@link NormalizedInputValue} which contains its type name and its current value * * @param name the name of the argument + * * @return an argument value */ public NormalizedInputValue getNormalizedArgument(String name) { @@ -407,6 +413,7 @@ public List getChildren() { * Returns the list of child fields that would have the same result key * * @param resultKey the result key to check + * * @return a list of all direct {@link ExecutableNormalizedField} children with the specified result key */ public List getChildrenWithSameResultKey(String resultKey) { @@ -427,6 +434,7 @@ public List getChildren(int includingRelativeLevel) { * This returns the child fields that can be used if the object is of the specified object type * * @param objectTypeName the object type + * * @return a list of child fields that would apply to that object type */ public List getChildren(String objectTypeName) { @@ -559,6 +567,7 @@ public static Builder newNormalizedField() { * Allows this {@link ExecutableNormalizedField} to be transformed via a {@link Builder} consumer callback * * @param builderConsumer the consumer given a builder + * * @return a new transformed {@link ExecutableNormalizedField} */ public ExecutableNormalizedField transform(Consumer builderConsumer) { diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index c70e5de517..2f4a6eb8d1 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -105,6 +105,7 @@ public static Options defaultOptions() { * e.g. can be passed to {@link graphql.schema.Coercing} for parsing. * * @param locale the locale to use + * * @return new options object to use */ public Options locale(Locale locale) { @@ -117,6 +118,7 @@ public Options locale(Locale locale) { * Can be used to intercept input values e.g. using {@link graphql.execution.values.InputInterceptor}. * * @param graphQLContext the context to use + * * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { @@ -128,6 +130,7 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * against malicious operations. * * @param maxChildrenDepth the max depth + * * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { @@ -138,6 +141,7 @@ public Options maxChildrenDepth(int maxChildrenDepth) { * Controls whether defer execution is supported when creating instances of {@link ExecutableNormalizedOperation}. * * @param deferSupport true to enable support for defer + * * @return new options object to use */ @ExperimentalApi @@ -147,6 +151,7 @@ public Options deferSupport(boolean deferSupport) { /** * @return context to use during operation parsing + * * @see #graphQLContext(GraphQLContext) */ public GraphQLContext getGraphQLContext() { @@ -155,6 +160,7 @@ public GraphQLContext getGraphQLContext() { /** * @return locale to use during operation parsing + * * @see #locale(Locale) */ public Locale getLocale() { @@ -163,6 +169,7 @@ public Locale getLocale() { /** * @return maximum children depth before aborting parsing + * * @see #maxChildrenDepth(int) */ public int getMaxChildrenDepth() { @@ -171,6 +178,7 @@ public int getMaxChildrenDepth() { /** * @return whether support for defer is enabled + * * @see #deferSupport(boolean) */ @ExperimentalApi @@ -190,6 +198,7 @@ public boolean getDeferSupport() { * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param coercedVariableValues the coerced variables to use + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( @@ -215,6 +224,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param operationName the operation name to use * @param coercedVariableValues the coerced variables to use * @param options the {@link Options} to use for parsing + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( @@ -241,6 +251,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param operationDefinition the operation to be executed * @param fragments a set of fragments associated with the operation * @param coercedVariableValues the coerced variables to use + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperation(GraphQLSchema graphQLSchema, @@ -263,6 +274,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( * @param document the {@link Document} holding the operation text * @param operationName the operation name to use * @param rawVariables the raw variables to be coerced + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, @@ -287,6 +299,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param rawVariables the raw variables that have not yet been coerced * @param locale the {@link Locale} to use during coercion * @param graphQLContext the {@link GraphQLContext} to use during coercion + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables( @@ -315,6 +328,7 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperationW * @param operationName the operation name to use * @param rawVariables the raw variables that have not yet been coerced * @param options the {@link Options} to use for parsing + * * @return a runtime representation of the graphql operation. */ public static ExecutableNormalizedOperation createExecutableNormalizedOperationWithRawVariables(GraphQLSchema graphQLSchema, diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 6d35061965..f65ab4a6f5 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -97,6 +97,7 @@ public Map getVariables() { * @param operationName the name of the operation to use * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -119,6 +120,7 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation + * * @return a {@link CompilerResult} object */ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -143,7 +145,9 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work + * * @return a {@link CompilerResult} object + * * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, VariablePredicate) */ @ExperimentalApi @@ -169,7 +173,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work + * * @return a {@link CompilerResult} object + * * @see ExecutableNormalizedOperationToAstCompiler#compileToDocument(GraphQLSchema, OperationDefinition.Operation, String, List, Map, VariablePredicate) */ @ExperimentalApi @@ -498,8 +504,12 @@ public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } ExecutionFragmentDetails that = (ExecutionFragmentDetails) o; return Objects.equals(typeName, that.typeName) && Objects.equals(deferExecution, that.deferExecution); } From 202573fd6ac6fb4dadd6cd7579c25d5a50d06472 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 2 Jan 2024 18:14:29 +1100 Subject: [PATCH 19/33] Fix edge case --- .../ExecutableNormalizedOperationFactory.java | 61 ++++++++++- ...tableNormalizedOperationToAstCompiler.java | 9 +- ...NormalizedOperationFactoryDeferTest.groovy | 101 +++++++++++++++++- ...izedOperationToAstCompilerDeferTest.groovy | 9 -- 4 files changed, 161 insertions(+), 19 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 2f4a6eb8d1..62c5825292 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -27,6 +27,7 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; +import graphql.language.TypeName; import graphql.language.VariableDefinition; import graphql.normalized.incremental.DeferExecution; import graphql.normalized.incremental.IncrementalNodes; @@ -42,19 +43,24 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; @@ -66,6 +72,7 @@ import static graphql.util.FpKit.intersection; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toSet; /** @@ -712,8 +719,9 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build(); - + Set deferExecutions = deferExecutionsBuilder.build().stream() + .filter(distinctByNullLabelAndType()) + .collect(toCollection(LinkedHashSet::new)); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -726,14 +734,61 @@ private List groupByCommonParentsWithDeferSupport(Collectio if (groupByAstParent.size() == 1) { return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, deferExecutions)); } + ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), deferExecutions)); + + Set filteredDeferExecutions = deferExecutions.stream() + .filter(deferExecution -> deferExecution.getTargetType() == null || + deferExecution.getTargetType().getName().equals(objectType.getName())) + .collect(toCollection(LinkedHashSet::new)); + + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); } return result.build(); } + /** + * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. + * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. + * For example, this query: + *
+     *     query example {
+     *        ... @defer {
+     *            name
+     *        }
+     *        ... @defer {
+     *            name
+     *        }
+     *     }
+     * 
+ * should result on single ENF. Essentially: + *
+     *     query example {
+     *        ... @defer {
+     *            name
+     *        }
+     *     }
+     * 
+ */ + private static @NotNull Predicate distinctByNullLabelAndType() { + Map seen = new ConcurrentHashMap<>(); + + return deferExecution -> { + if (deferExecution.getLabel() == null) { + String typeName = Optional.ofNullable(deferExecution.getTargetType()) + .map(TypeName::getName) + .map(String::toUpperCase) + .orElse("null"); + + return seen.putIfAbsent(typeName, Boolean.TRUE) == null; + } + + return true; + }; + } + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() .map(DeferExecution::getLabel) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index f65ab4a6f5..861ded72f5 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -291,12 +291,9 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor .add(field); } else { deferExecutions.forEach(deferExecution -> { - if (deferExecution.getTargetType() == null || - objectTypeName.equals(deferExecution.getTargetType().getName())) { - fieldsByFragmentDetails - .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) - .add(field); - } + fieldsByFragmentDetails + .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, deferExecution), ignored -> new ArrayList<>()) + .add(field); }); } }); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 9c184e5966..2fad250b50 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -32,9 +32,21 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { owner: Person } + type Cat implements Animal { + name: String + breed: String + color: String + siblings: [Cat] + } + + type Fish implements Animal { + name: String + } + type Person { firstname: String lastname: String + bestFriend: Person } """ @@ -66,6 +78,64 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "fragments on non-conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + name + } + ... on Dog @defer { + name + } + ... on Animal @defer { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer[null]', + ] + } + + def "fragments on conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + breed + } + ... on Dog @defer { + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + 'Cat.breed defer[null]', + 'Dog.breed defer[null]' + ] + } + def "defer on a single field via inline fragment with type"() { given: @@ -233,6 +303,35 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "multiple fields and multiple defers - no label"() { + given: + + String query = ''' + query q { + dog { + ... @defer { + name + } + + ... @defer { + name + } + } + } + + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.dog', + 'Dog.name defer[null]', + ] + } + def "multiple fields and a multiple defers with same label are not allowed"() { given: @@ -354,7 +453,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - 'Dog.name', + '[Cat, Dog, Fish].name', 'Dog.owner defer[dog-defer]', 'Person.firstname', 'Person.lastname defer[lastname-defer]', diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index d2c88489b6..090fc26413 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -204,9 +204,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { breed } - ... @defer { - breed - } } } ''' @@ -307,9 +304,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { name } - ... @defer { - name - } } } ''' @@ -454,9 +448,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { - ... @defer { - name - } ... @defer { name } From 4db220e19eb73badb23bb8b710d0b1f011c6364f Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 2 Jan 2024 18:27:50 +1100 Subject: [PATCH 20/33] Replace Guava's Multimap with Map in public APIs --- .../ExecutableNormalizedOperation.java | 5 +++-- ...tableNormalizedOperationToAstCompiler.java | 22 +++++++++---------- ...NormalizedOperationFactoryDeferTest.groovy | 2 +- ...ormalizedOperationToAstCompilerTest.groovy | 4 ++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index 9bfef00b26..9440620c49 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -13,6 +13,7 @@ import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLFieldsContainer; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -135,8 +136,8 @@ public Map getNormalizedFieldToQuery * @return a map of {@link ExecutableNormalizedField} to its {@link DeferExecution} */ @ExperimentalApi - public ImmutableListMultimap getNormalizedFieldToDeferExecution() { - return normalizedFieldToDeferExecution; + public Map> getNormalizedFieldToDeferExecution() { + return normalizedFieldToDeferExecution.asMap(); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index 861ded72f5..b4ce04db18 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -1,7 +1,6 @@ package graphql.normalized; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import graphql.Assert; import graphql.Directives; @@ -34,6 +33,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -156,7 +156,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @Nullable VariablePredicate variablePredicate, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); } @@ -185,7 +185,7 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); } @@ -195,7 +195,7 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); @@ -222,7 +222,7 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { if (normalizedFieldToDeferExecution != null) { return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); } else { @@ -270,7 +270,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - @NotNull ImmutableListMultimap normalizedFieldToDeferExecution, + @NotNull Map> normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -280,12 +280,12 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - List deferExecutions = normalizedFieldToDeferExecution.get(nf); + Collection deferExecutions = normalizedFieldToDeferExecution.get(nf); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution) .forEach((objectTypeName, field) -> { - if (deferExecutions.isEmpty()) { + if (deferExecutions == null || deferExecutions.isEmpty()) { fieldsByFragmentDetails .computeIfAbsent(new ExecutionFragmentDetails(objectTypeName, null), ignored -> new ArrayList<>()) .add(field); @@ -298,7 +298,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor } }); - } else if (!deferExecutions.isEmpty()) { + } else if (deferExecutions != null && !deferExecutions.isEmpty()) { Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); deferExecutions.forEach(deferExecution -> { @@ -344,7 +344,7 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { @@ -362,7 +362,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable ImmutableListMultimap normalizedFieldToDeferExecution) { + @Nullable Map> normalizedFieldToDeferExecution) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 2fad250b50..a8b96e0fc8 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -606,7 +606,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { String printDeferExecutionDetails(ExecutableNormalizedField field) { def deferExecutions = normalizedFieldToDeferExecution.get(field) - if (deferExecutions.isEmpty()) { + if (deferExecutions == null || deferExecutions.isEmpty()) { return "" } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index e0720dca3c..99079c8705 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2184,7 +2184,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat String operationName, List topLevelFields, VariablePredicate variablePredicate, - ImmutableListMultimap normalizedFieldToDeferExecution + Map> normalizedFieldToDeferExecution ) { return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution) } @@ -2196,7 +2196,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat List topLevelFields, Map normalizedFieldToQueryDirectives, VariablePredicate variablePredicate, - ImmutableListMultimap normalizedFieldToDeferExecution + Map> normalizedFieldToDeferExecution ) { if (deferSupport) { return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution) From 6446ad484950b41d461835a9f7cdd2d4f2040e87 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Fri, 5 Jan 2024 14:07:28 +1100 Subject: [PATCH 21/33] Big refactoring: - introduce NormalizedDeferExecution - add defer execution state to ENF --- .../java/graphql/normalized/ENFMerger.java | 22 +- .../normalized/ExecutableNormalizedField.java | 27 ++ .../ExecutableNormalizedOperation.java | 17 +- .../ExecutableNormalizedOperationFactory.java | 83 ++-- ...tableNormalizedOperationToAstCompiler.java | 69 ++-- .../incremental/DeferDeclaration.java | 35 ++ .../incremental/DeferExecution.java | 36 -- .../incremental/IncrementalNodes.java | 8 +- .../incremental/NormalizedDeferExecution.java | 43 +++ .../NormalizedDeferExecutionFactory.java | 119 ++++++ ...NormalizedOperationFactoryDeferTest.groovy | 363 ++++++++++++++++-- ...izedOperationToAstCompilerDeferTest.groovy | 51 +-- ...ormalizedOperationToAstCompilerTest.groovy | 44 +-- 13 files changed, 683 insertions(+), 234 deletions(-) create mode 100644 src/main/java/graphql/normalized/incremental/DeferDeclaration.java delete mode 100644 src/main/java/graphql/normalized/incremental/DeferExecution.java create mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java create mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index 97d182a5f4..d876cf56db 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,9 +1,11 @@ package graphql.normalized; +import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; +import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -19,7 +21,12 @@ @Internal public class ENFMerger { - public static void merge(ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema) { + public static void merge( + ExecutableNormalizedField parent, + List childrenWithSameResultKey, + GraphQLSchema schema, + Multimap normalizedFieldToDeferExecution + ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same List> possibleGroupsToMerge = new ArrayList<>(); @@ -28,7 +35,7 @@ public static void merge(ExecutableNormalizedField parent, List group : possibleGroupsToMerge) { for (ExecutableNormalizedField fieldInGroup : group) { - if(field.getFieldName().equals(Introspection.TypeNameMetaFieldDef.getName())) { + if (field.getFieldName().equals(Introspection.TypeNameMetaFieldDef.getName())) { addToGroup = true; group.add(field); continue overPossibleGroups; @@ -58,13 +65,18 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) } boolean mergeable = areFieldSetsTheSame(listOfChildrenForGroup); if (mergeable) { - Set mergedObjects = new LinkedHashSet<>(); - groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); // patching the first one to contain more objects, remove all others Iterator iterator = groupOfFields.iterator(); ExecutableNormalizedField first = iterator.next(); + + Set mergedObjects = new LinkedHashSet<>(); + groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); while (iterator.hasNext()) { - parent.getChildren().remove(iterator.next()); + ExecutableNormalizedField next = iterator.next(); + // Move defer executions from removed field into the merged field's entry + normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); + parent.getChildren().remove(next); + normalizedFieldToDeferExecution.removeAll(next); } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 78b4c6d8e4..93f128fa59 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -2,13 +2,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import graphql.Assert; +import graphql.ExperimentalApi; import graphql.Internal; import graphql.Mutable; import graphql.PublicApi; import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; +import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -63,6 +66,9 @@ public class ExecutableNormalizedField { private final String fieldName; private final int level; + // Mutable List on purpose: it is modified after creation + private final LinkedHashSet deferExecutions; + private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; this.resolvedArguments = builder.resolvedArguments; @@ -73,6 +79,7 @@ private ExecutableNormalizedField(Builder builder) { this.children = builder.children; this.level = builder.level; this.parent = builder.parent; + this.deferExecutions = builder.deferExecutions; } /** @@ -255,6 +262,12 @@ public void clearChildren() { this.children.clear(); } + @Internal + public void setDeferExecutions(Collection deferExecutions) { + this.deferExecutions.clear(); + this.deferExecutions.addAll(deferExecutions); + } + /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -460,6 +473,12 @@ public ExecutableNormalizedField getParent() { return parent; } + // TODO: Javadoc + @ExperimentalApi + public LinkedHashSet getDeferExecutions() { + return deferExecutions; + } + @Internal public void replaceParent(ExecutableNormalizedField newParent) { this.parent = newParent; @@ -587,6 +606,8 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private Builder() { } @@ -600,6 +621,7 @@ private Builder(ExecutableNormalizedField existing) { this.children = new ArrayList<>(existing.children); this.level = existing.getLevel(); this.parent = existing.getParent(); + this.deferExecutions = existing.getDeferExecutions(); } public Builder clearObjectTypesNames() { @@ -655,6 +677,11 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } + public Builder deferExecutions(LinkedHashSet deferExecutions) { + this.deferExecutions = deferExecutions; + return this; + } + public ExecutableNormalizedField build() { return new ExecutableNormalizedField(this); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java index 9440620c49..ce50c9931b 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperation.java @@ -2,18 +2,15 @@ import com.google.common.collect.ImmutableListMultimap; import graphql.Assert; -import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.MergedField; import graphql.execution.ResultPath; import graphql.execution.directives.QueryDirectives; import graphql.language.Field; import graphql.language.OperationDefinition; -import graphql.normalized.incremental.DeferExecution; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLFieldsContainer; -import java.util.Collection; import java.util.List; import java.util.Map; @@ -33,7 +30,6 @@ public class ExecutableNormalizedOperation { private final ImmutableListMultimap fieldToNormalizedField; private final Map normalizedFieldToMergedField; private final Map normalizedFieldToQueryDirectives; - private final ImmutableListMultimap normalizedFieldToDeferExecution; private final ImmutableListMultimap coordinatesToNormalizedFields; public ExecutableNormalizedOperation( @@ -43,8 +39,7 @@ public ExecutableNormalizedOperation( ImmutableListMultimap fieldToNormalizedField, Map normalizedFieldToMergedField, Map normalizedFieldToQueryDirectives, - ImmutableListMultimap coordinatesToNormalizedFields, - ImmutableListMultimap normalizedFieldToDeferExecution + ImmutableListMultimap coordinatesToNormalizedFields ) { this.operation = operation; this.operationName = operationName; @@ -53,7 +48,6 @@ public ExecutableNormalizedOperation( this.normalizedFieldToMergedField = normalizedFieldToMergedField; this.normalizedFieldToQueryDirectives = normalizedFieldToQueryDirectives; this.coordinatesToNormalizedFields = coordinatesToNormalizedFields; - this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } /** @@ -132,15 +126,6 @@ public Map getNormalizedFieldToQuery } - /** - * @return a map of {@link ExecutableNormalizedField} to its {@link DeferExecution} - */ - @ExperimentalApi - public Map> getNormalizedFieldToDeferExecution() { - return normalizedFieldToDeferExecution.asMap(); - } - - /** * This looks up the {@link QueryDirectives} associated with the given {@link ExecutableNormalizedField} * diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 62c5825292..0ec0891ab8 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,6 +4,8 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.LinkedHashMultimap; import graphql.Assert; import graphql.ExperimentalApi; import graphql.GraphQLContext; @@ -27,10 +29,10 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; -import graphql.language.TypeName; import graphql.language.VariableDefinition; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.DeferDeclaration; import graphql.normalized.incremental.IncrementalNodes; +import graphql.normalized.incremental.NormalizedDeferExecutionFactory; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -410,7 +412,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; - ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); Consumer captureCollectNFResult = (collectNFResult -> { @@ -440,8 +442,13 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); } + + if (options.deferSupport) { + NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); + } + return new ExecutableNormalizedOperation( operationDefinition.getOperation(), operationDefinition.getName(), @@ -449,8 +456,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr fieldToNormalizedField.build(), normalizedFieldToMergedField.build(), normalizedFieldToQueryDirectives.build(), - coordinatesToNormalizedFields.build(), - normalizedFieldToDeferExecution.build() + coordinatesToNormalizedFields.build() ); } @@ -529,9 +535,13 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; - private final ImmutableListMultimap normalizedFieldToDeferExecution; + private final ImmutableSetMultimap normalizedFieldToDeferExecution; - public CollectNFResult(Collection children, ImmutableListMultimap normalizedFieldToAstFields, ImmutableListMultimap normalizedFieldToDeferExecution) { + public CollectNFResult( + Collection children, + ImmutableListMultimap normalizedFieldToAstFields, + ImmutableSetMultimap normalizedFieldToDeferExecution + ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; @@ -547,7 +557,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableListMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -568,7 +578,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); @@ -594,7 +604,7 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableListMultimap.Builder normalizedFieldToDeferExecution = ImmutableListMultimap.builder(); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); @@ -613,7 +623,7 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, - ImmutableListMultimap.Builder normalizedFieldToDeferExecution, + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -669,9 +679,9 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p private static class CollectedFieldGroup { Set objectTypes; Set fields; - Set deferExecutions; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; this.deferExecutions = deferExecutions; @@ -706,12 +716,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferExecution collectedDeferExecution = collectedField.deferExecution; + DeferDeclaration collectedDeferExecution = collectedField.deferExecution; if (collectedDeferExecution != null) { deferExecutionsBuilder.add(collectedDeferExecution); @@ -719,7 +729,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build().stream() + Set deferExecutions = deferExecutionsBuilder.build().stream() .filter(distinctByNullLabelAndType()) .collect(toCollection(LinkedHashSet::new)); @@ -739,9 +749,8 @@ private List groupByCommonParentsWithDeferSupport(Collectio for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - Set filteredDeferExecutions = deferExecutions.stream() - .filter(deferExecution -> deferExecution.getTargetType() == null || - deferExecution.getTargetType().getName().equals(objectType.getName())) + Set filteredDeferExecutions = deferExecutions.stream() + .filter(filter(objectType)) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -749,6 +758,21 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } + private static Predicate filter(GraphQLObjectType objectType) { + return deferExecution -> { + if (deferExecution.getTargetType() == null) { + return true; + } + + if (deferExecution.getTargetType().equals(objectType.getName())) { + return true; + } + + return objectType.getInterfaces().stream() + .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); + }; + } + /** * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. @@ -772,13 +796,12 @@ private List groupByCommonParentsWithDeferSupport(Collectio * } * */ - private static @NotNull Predicate distinctByNullLabelAndType() { + private static @NotNull Predicate distinctByNullLabelAndType() { Map seen = new ConcurrentHashMap<>(); return deferExecution -> { if (deferExecution.getLabel() == null) { String typeName = Optional.ofNullable(deferExecution.getTargetType()) - .map(TypeName::getName) .map(String::toUpperCase) .orElse("null"); @@ -789,9 +812,9 @@ private List groupByCommonParentsWithDeferSupport(Collectio }; } - private Set listDuplicatedLabels(Collection deferExecutions) { + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() - .map(DeferExecution::getLabel) + .map(DeferDeclaration::getLabel) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() @@ -806,7 +829,7 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferExecution deferExecution + DeferDeclaration deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { @@ -823,9 +846,9 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; - DeferExecution deferExecution; + DeferDeclaration deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferDeclaration deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; @@ -861,7 +884,7 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), fragmentSpread.getDirectives(), fragmentDefinition.getTypeCondition() @@ -890,7 +913,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), inlineFragment.getTypeCondition() @@ -904,7 +927,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferExecution deferExecution + DeferDeclaration deferExecution ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index b4ce04db18..cae6f42ae7 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,7 +23,8 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferExecution; +import graphql.normalized.incremental.DeferDeclaration; +import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -35,6 +36,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -129,7 +131,7 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, null); + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, false); } @@ -139,12 +141,11 @@ public static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, * The {@link VariablePredicate} is used called to decide if the given argument values should be made into a variable * OR inlined into the operation text as a graphql literal. * - * @param schema the graphql schema to use - * @param operationKind the kind of operation - * @param operationName the name of the operation to use - * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from - * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work + * @param schema the graphql schema to use + * @param operationKind the kind of operation + * @param operationName the name of the operation to use + * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from + * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation * * @return a {@link CompilerResult} object * @@ -155,9 +156,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @NotNull OperationDefinition.Operation operationKind, @Nullable String operationName, @NotNull List topLevelFields, - @Nullable VariablePredicate variablePredicate, - @Nullable Map> normalizedFieldToDeferExecution) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution); + @Nullable VariablePredicate variablePredicate + ) { + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate); } /** @@ -172,7 +173,6 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS * @param topLevelFields the top level {@link ExecutableNormalizedField}s to start from * @param normalizedFieldToQueryDirectives the map of normalized field to query directives * @param variablePredicate the variable predicate that decides if arguments turn into variables or not during compilation - * @param normalizedFieldToDeferExecution a map associating {@link ExecutableNormalizedField}s with their {@link DeferExecution}s, this parameter needs to be present in order for defer support to work * * @return a {@link CompilerResult} object * @@ -184,9 +184,9 @@ public static CompilerResult compileToDocumentWithDeferSupport(@NotNull GraphQLS @Nullable String operationName, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, - @Nullable VariablePredicate variablePredicate, - @Nullable Map> normalizedFieldToDeferExecution) { - return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution); + @Nullable VariablePredicate variablePredicate + ) { + return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, true); } private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @@ -195,11 +195,11 @@ private static CompilerResult compileToDocument(@NotNull GraphQLSchema schema, @NotNull List topLevelFields, @NotNull Map normalizedFieldToQueryDirectives, @Nullable VariablePredicate variablePredicate, - @Nullable Map> normalizedFieldToDeferExecution) { + boolean deferSupport) { GraphQLObjectType operationType = getOperationType(schema, operationKind); VariableAccumulator variableAccumulator = new VariableAccumulator(variablePredicate); - List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); + List> selections = subselectionsForNormalizedField(schema, operationType.getName(), topLevelFields, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport); SelectionSet selectionSet = new SelectionSet(selections); OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition() @@ -222,9 +222,9 @@ private static List> subselectionsForNormalizedField(GraphQLSchema List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map> normalizedFieldToDeferExecution) { - if (normalizedFieldToDeferExecution != null) { - return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, normalizedFieldToDeferExecution, variableAccumulator); + boolean deferSupport) { + if (deferSupport) { + return subselectionsForNormalizedFieldWithDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); } else { return subselectionsForNormalizedFieldNoDeferSupport(schema, parentOutputType, executableNormalizedFields, normalizedFieldToQueryDirectives, variableAccumulator); } @@ -243,13 +243,13 @@ private static List> subselectionsForNormalizedFieldNoDeferSupport( for (ExecutableNormalizedField nf : executableNormalizedFields) { if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, null) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, false) .forEach((objectTypeName, field) -> fieldsByTypeCondition .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>()) .add(field)); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, null)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, false)); } } @@ -270,7 +270,6 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor @NotNull String parentOutputType, List executableNormalizedFields, @NotNull Map normalizedFieldToQueryDirectives, - @NotNull Map> normalizedFieldToDeferExecution, VariableAccumulator variableAccumulator) { ImmutableList.Builder> selections = ImmutableList.builder(); @@ -280,10 +279,10 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - Collection deferExecutions = normalizedFieldToDeferExecution.get(nf); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { - selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution) + selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) .forEach((objectTypeName, field) -> { if (deferExecutions == null || deferExecutions.isEmpty()) { fieldsByFragmentDetails @@ -299,7 +298,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor }); } else if (deferExecutions != null && !deferExecutions.isEmpty()) { - Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution); + Field field = selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true); deferExecutions.forEach(deferExecution -> { fieldsByFragmentDetails @@ -307,7 +306,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor .add(field); }); } else { - selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); + selections.add(selectionForNormalizedField(schema, parentOutputType, nf, normalizedFieldToQueryDirectives, variableAccumulator, true)); } } @@ -323,8 +322,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); + if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -344,11 +343,11 @@ private static Map selectionForNormalizedField(GraphQLSchema sche ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map> normalizedFieldToDeferExecution) { + boolean deferSupport) { Map groupedFields = new LinkedHashMap<>(); for (String objectTypeName : executableNormalizedField.getObjectTypeNames()) { - groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, normalizedFieldToDeferExecution)); + groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, executableNormalizedField, normalizedFieldToQueryDirectives, variableAccumulator, deferSupport)); } return groupedFields; @@ -362,7 +361,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, ExecutableNormalizedField executableNormalizedField, @NotNull Map normalizedFieldToQueryDirectives, VariableAccumulator variableAccumulator, - @Nullable Map> normalizedFieldToDeferExecution) { + boolean deferSupport) { final List> subSelections; if (executableNormalizedField.getChildren().isEmpty()) { subSelections = emptyList(); @@ -376,7 +375,7 @@ private static Field selectionForNormalizedField(GraphQLSchema schema, executableNormalizedField.getChildren(), normalizedFieldToQueryDirectives, variableAccumulator, - normalizedFieldToDeferExecution + deferSupport ); } @@ -492,9 +491,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferExecution deferExecution; + private final NormalizedDeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java new file mode 100644 index 0000000000..688b9dd8a8 --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java @@ -0,0 +1,35 @@ +package graphql.normalized.incremental; + +import graphql.Internal; + +import javax.annotation.Nullable; + +/** + * TODO: Javadoc + */ +@Internal +public class DeferDeclaration { + private final String label; + private final String targetType; + + public DeferDeclaration(@Nullable String label, @Nullable String targetType) { + this.label = label; + this.targetType = targetType; + } + + /** + * @return the label associated with this defer declaration + */ + @Nullable + public String getLabel() { + return label; + } + + /** + * @return the name of the type that is the target of the defer declaration + */ + @Nullable + public String getTargetType() { + return targetType; + } +} diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java deleted file mode 100644 index 8d62c0acaa..0000000000 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ /dev/null @@ -1,36 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.ExperimentalApi; -import graphql.language.TypeName; - -import javax.annotation.Nullable; - -/** - * Represents the defer execution aspect of a query field - */ -@ExperimentalApi -public class DeferExecution { - private final String label; - private final TypeName targetType; - - public DeferExecution(@Nullable String label, @Nullable TypeName targetType) { - this.label = label; - this.targetType = targetType; - } - - /** - * @return the label associated with this defer execution - */ - @Nullable - public String getLabel() { - return label; - } - - /** - * @return the {@link TypeName} of the type that is the target of the defer execution - */ - @Nullable - public TypeName getTargetType() { - return targetType; - } -} diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 9898616ddc..5b3f240d36 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -19,7 +19,7 @@ @Internal public class IncrementalNodes { - public DeferExecution getDeferExecution( + public DeferDeclaration getDeferExecution( Map variables, List directives, @Nullable TypeName targetType @@ -38,13 +38,15 @@ public DeferExecution getDeferExecution( Object label = argumentValues.get("label"); + String targetTypeName = targetType == null ? null : targetType.getName(); + if (label == null) { - return new DeferExecution(null, targetType); + return new DeferDeclaration(null, targetTypeName); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label, targetType); + return new DeferDeclaration((String) label, targetTypeName); } diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java new file mode 100644 index 0000000000..aad493018b --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java @@ -0,0 +1,43 @@ +package graphql.normalized.incremental; + +import graphql.ExperimentalApi; + +import javax.annotation.Nullable; +import java.util.Set; + +/** + * Represents the aspects of defer that are important for runtime execution. + */ +@ExperimentalApi +public class NormalizedDeferExecution { + private final DeferBlock deferBlock; + private final Set objectTypeNames; + + public NormalizedDeferExecution(DeferBlock deferBlock, Set objectTypeNames) { + this.deferBlock = deferBlock; + this.objectTypeNames = objectTypeNames; + } + + public Set getObjectTypeNames() { + return objectTypeNames; + } + + public DeferBlock getDeferBlock() { + return deferBlock; + } + + // TODO: Javadoc + @ExperimentalApi + public static class DeferBlock { + private final String label; + + public DeferBlock(@Nullable String label) { + this.label = label; + } + + @Nullable + public String getLabel() { + return label; + } + } +} diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java new file mode 100644 index 0000000000..0ef085332b --- /dev/null +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -0,0 +1,119 @@ +package graphql.normalized.incremental; + +import com.google.common.collect.Multimap; +import graphql.Internal; +import graphql.normalized.ExecutableNormalizedField; +import graphql.normalized.incremental.NormalizedDeferExecution.DeferBlock; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +// TODO: Javadoc +@Internal +public class NormalizedDeferExecutionFactory { + public static void normalizeDeferExecutions( + GraphQLSchema graphQLSchema, + Multimap normalizedFieldDeferExecution + ) { + new DeferExecutionMergerInner(graphQLSchema, normalizedFieldDeferExecution).execute(); + } + + private static class DeferExecutionMergerInner { + private final GraphQLSchema graphQLSchema; + private final Multimap input; + + private DeferExecutionMergerInner( + GraphQLSchema graphQLSchema, + Multimap normalizedFieldToDeferExecution + ) { + this.graphQLSchema = graphQLSchema; + this.input = normalizedFieldToDeferExecution; + } + + private void execute() { + Map declarationToBlock = new HashMap<>(); + + this.input.keySet().forEach(field -> { + Collection executionsForField = input.get(field); + + Set fieldTypes = field.getObjectTypeNames().stream() + .map(graphQLSchema::getType) + .filter(GraphQLObjectType.class::isInstance) + .map(GraphQLObjectType.class::cast) + .collect(Collectors.toSet()); + + Set fieldTypeNames = fieldTypes.stream().map(GraphQLObjectType::getName).collect(Collectors.toSet()); + + Map, List> executionsByLabel = executionsForField.stream() + .collect(Collectors.groupingBy(execution -> Optional.ofNullable(execution.getLabel()))); + + Set deferExecutions = executionsByLabel.keySet().stream() + .map(label -> { + List executionsForLabel = executionsByLabel.get(label); + + DeferBlock deferBlock = executionsForLabel.stream() + .map(declarationToBlock::get) + .filter(Objects::nonNull) + .findFirst() + .orElse(new DeferBlock(label.orElse(null))); + + Set types = executionsForLabel.stream() + .map(deferExecution -> { + declarationToBlock.put(deferExecution, deferBlock); + return deferExecution.getTargetType(); + }) + .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) + .collect(Collectors.toSet()); + + return new NormalizedDeferExecution(deferBlock, types); + + }) + .collect(Collectors.toSet()); + + if (!deferExecutions.isEmpty()) { + // Mutate the field, by setting deferExecutions + field.setDeferExecutions(deferExecutions); + } + }); + } + + @NotNull + private Function> mapToPossibleTypes(Set fieldTypeNames, Set fieldTypes) { + return typeName -> { + if (typeName == null) { + return fieldTypeNames.stream(); + } + + GraphQLType type = graphQLSchema.getType(typeName); + + if (type instanceof GraphQLInterfaceType) { + return fieldTypes.stream() + .filter(filterImplementsInterface((GraphQLInterfaceType) type)) + .map(GraphQLObjectType::getName); + } + + return Stream.of(typeName); + }; + } + + private static Predicate filterImplementsInterface(GraphQLInterfaceType interfaceType) { + return objectType -> objectType.getInterfaces().stream() + .anyMatch(implementedInterface -> implementedInterface.getName().equals(interfaceType.getName())); + } + } + +} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index a8b96e0fc8..eb2260ee11 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -20,27 +20,36 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { type Query { dog: Dog animal: Animal + mammal: Mammal } - interface Animal { + interface LivingThing { + age: Int + } + + interface Animal implements LivingThing { name: String + age: Int } - type Dog implements Animal { + type Dog implements Animal & LivingThing { name: String + age: Int breed: String owner: Person } - type Cat implements Animal { + type Cat implements Animal & LivingThing { name: String + age: Int breed: String color: String siblings: [Cat] } - type Fish implements Animal { + type Fish implements Animal & LivingThing { name: String + age: Int } type Person { @@ -48,6 +57,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { lastname: String bestFriend: Person } + + union Mammal = Dog | Cat """ GraphQLSchema graphQLSchema = TestUtil.schema(schema) @@ -74,7 +85,31 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', 'Dog.name', - 'Dog.breed defer[breed-defer]', + 'Dog.breed defer{[label=breed-defer;types=[Dog]]}', + ] + } + + def "fragment on interface field with no type"() { + given: + + String query = ''' + query q { + animal { + ... @defer { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}", ] } @@ -104,7 +139,171 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - '[Cat, Dog, Fish].name defer[null]', + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}", + ] + } + + def "fragments on subset of non-conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + name + } + ... on Dog @defer { + name + } + ... on Fish { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog]]}", + ] + } + + def "fragment on interface"() { + given: + + String query = ''' + query q { + animal { + ... on Animal @defer { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + ] + } + + def "fragment on distant interface"() { + given: + + String query = ''' + query q { + animal { + ... on LivingThing @defer { + age + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].age defer{[label=null;types=[Cat, Dog, Fish]]}', + ] + } + + def "fragment on union"() { + given: + + String query = ''' + query q { + mammal { + ... on Dog @defer { + name + breed + } + ... on Cat @defer { + name + breed + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.mammal', + '[Dog, Cat].name defer{[label=null;types=[Cat, Dog]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}', + 'Cat.breed defer{[label=null;types=[Cat]]}', + ] + } + + def "fragments on interface"() { + given: + + String query = ''' + query q { + animal { + ... on Animal @defer { + name + } + ... on Animal @defer { + age + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + '[Cat, Dog, Fish].age defer{[label=null;types=[Cat, Dog, Fish]]}', + ] + } + + def "defer on a subselection of non-conditional fields"() { + given: + + String query = ''' + query q { + animal { + ... on Cat @defer { + name + } + ... on Dog { + name + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + '[Cat, Dog].name defer{[label=null;types=[Cat]]}', ] } @@ -131,8 +330,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - 'Cat.breed defer[null]', - 'Dog.breed defer[null]' + 'Cat.breed defer{[label=null;types=[Cat]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}' ] } @@ -158,18 +357,69 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', 'Dog.name', - 'Dog.breed defer[breed-defer]', + 'Dog.breed defer{[label=breed-defer;types=[Dog]]}', ] } - def "defer on 2 fields"() { + def "1 defer on 2 fields"() { + given: + String query = ''' + query q { + animal { + ... @defer { + name + } + + ... on Dog @defer { + name + breed + } + + ... on Cat @defer { + name + breed + } + } + } + ''' + + Map variables = [:] + + when: + def executableNormalizedOperation = createExecutableNormalizedOperations(query, variables); + + List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) + + then: "should result in the same instance of defer" + def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") + def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") + def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") + + nameField.deferExecutions.size() == 1 + dogBreedField.deferExecutions.size() == 1 + catBreedField.deferExecutions.size() == 1 + + // same label instances + nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock + dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock + + printedTree == ['Query.animal', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}', + 'Cat.breed defer{[label=null;types=[Cat]]}', + ] + } + + def "2 defers on 2 fields"() { given: String query = ''' query q { dog { - ... @defer(label: "breed-defer") { + ... @defer{ name + } + ... @defer{ breed } } @@ -179,12 +429,23 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { Map variables = [:] when: - List printedTree = executeQueryAndPrintTree(query, variables) + def executableNormalizedOperation = createExecutableNormalizedOperations(query, variables); + + List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) + + then: "should result in 2 different instances of defer" + def nameField = findField(executableNormalizedOperation, "Dog", "name") + def breedField = findField(executableNormalizedOperation, "Dog", "breed") + + nameField.deferExecutions.size() == 1 + breedField.deferExecutions.size() == 1 + + // different label instances + nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock - then: printedTree == ['Query.dog', - 'Dog.name defer[breed-defer]', - 'Dog.breed defer[breed-defer]', + 'Dog.name defer{[label=null;types=[Dog]]}', + 'Dog.breed defer{[label=null;types=[Dog]]}', ] } @@ -211,8 +472,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[breed-defer]', - 'Dog.breed defer[breed-defer]', + 'Dog.name defer{[label=breed-defer;types=[Dog]]}', + 'Dog.breed defer{[label=breed-defer;types=[Dog]]}', ] } @@ -241,7 +502,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[name-defer,another-name-defer]', + 'Dog.name defer{[label=another-name-defer;types=[Dog]],[label=name-defer;types=[Dog]]}' ] } @@ -270,7 +531,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[name-defer]', + 'Dog.name defer{[label=name-defer;types=[Dog]]}', ] } @@ -299,7 +560,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[null]', + 'Dog.name defer{[label=null;types=[Dog]]}', ] } @@ -328,7 +589,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[null]', + 'Dog.name defer{[label=null;types=[Dog]]}', ] } @@ -387,10 +648,10 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[null]', - 'Dog.owner defer[null]', + 'Dog.name defer{[label=null;types=[Dog]]}', + 'Dog.owner defer{[label=null;types=[Dog]]}', 'Person.firstname', - 'Person.lastname defer[null]', + 'Person.lastname defer{[label=null;types=[Person]]}', ] } @@ -420,10 +681,10 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[dog-defer]', - 'Dog.owner defer[dog-defer]', + 'Dog.name defer{[label=dog-defer;types=[Dog]]}', + 'Dog.owner defer{[label=dog-defer;types=[Dog]]}', 'Person.firstname', - 'Person.lastname defer[lastname-defer]', + 'Person.lastname defer{[label=lastname-defer;types=[Person]]}', ] } @@ -454,9 +715,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', '[Cat, Dog, Fish].name', - 'Dog.owner defer[dog-defer]', + 'Dog.owner defer{[label=dog-defer;types=[Dog]]}', 'Person.firstname', - 'Person.lastname defer[lastname-defer]', + 'Person.lastname defer{[label=lastname-defer;types=[Person]]}', ] } @@ -484,7 +745,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[three]', + 'Dog.name defer{[label=three;types=[Dog]]}', ] } @@ -513,7 +774,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[another-name-defer]', + 'Dog.name defer{[label=another-name-defer;types=[Dog]]}', ] } @@ -542,7 +803,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[another-name-defer]', + 'Dog.name defer{[label=another-name-defer;types=[Dog]]}', ] } @@ -571,10 +832,24 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer[name-defer]', + 'Dog.name defer{[label=name-defer;types=[Dog]]}', ] } + private ExecutableNormalizedOperation createExecutableNormalizedOperations(String query, Map variables) { + assertValidQuery(graphQLSchema, query, variables) + Document document = TestUtil.parseQuery(query) + ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + + return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + graphQLSchema, + document, + null, + RawVariables.of(variables), + ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(true), + ) + } + private List executeQueryAndPrintTree(String query, Map variables) { assertValidQuery(graphQLSchema, query, variables) Document document = TestUtil.parseQuery(query) @@ -592,9 +867,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { private List printTreeWithIncrementalExecutionDetails(ExecutableNormalizedOperation queryExecutionTree) { def result = [] - Traverser traverser = Traverser.depthFirst({ it.getChildren() }) - - def normalizedFieldToDeferExecution = queryExecutionTree.normalizedFieldToDeferExecution + Traverser traverser = Traverser.depthFirst({ it.getChildren() }) traverser.traverse(queryExecutionTree.getTopLevelFields(), new TraverserVisitorStub() { @Override @@ -605,25 +878,33 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } String printDeferExecutionDetails(ExecutableNormalizedField field) { - def deferExecutions = normalizedFieldToDeferExecution.get(field) + def deferExecutions = field.deferExecutions if (deferExecutions == null || deferExecutions.isEmpty()) { return "" } - def deferLabels = deferExecutions - .collect { it.label } + def deferLabels = new ArrayList<>(deferExecutions) + .sort { it.deferBlock.label } + .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } .join(",") - return " defer[" + deferLabels + "]" + return " defer{${deferLabels}}" } }) result } - private void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { + private static void assertValidQuery(GraphQLSchema graphQLSchema, String query, Map variables = [:]) { GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build() def ei = ExecutionInput.newExecutionInput(query).variables(variables).build() assert graphQL.execute(ei).errors.size() == 0 } + + private static ExecutableNormalizedField findField(ExecutableNormalizedOperation operation, String objectTypeNames, String fieldName) { + return operation.normalizedFieldToMergedField + .collect { it.key } + .find { it.fieldName == fieldName + && it.objectTypeNamesToString() == objectTypeNames} + } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 090fc26413..8d7f9e91a3 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -75,7 +75,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -89,33 +89,6 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ''' } - def "@defer directives are not generated when map is null"() { - String query = """ - query q { - dog { - name - ... @defer(label: "breed-defer") { - breed - } - } - } - """ - GraphQLSchema schema = mkSchema(sdl) - def tree = createNormalizedTree(schema, query) - def normalizedFieldToDeferExecution = null - when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, normalizedFieldToDeferExecution) - def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) - then: - printed == '''{ - dog { - breed - name - } -} -''' - } - def "simple defer with named spread"() { String query = """ query q { @@ -130,7 +103,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -161,7 +134,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -195,7 +168,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -225,7 +198,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -259,7 +232,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -296,7 +269,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -325,7 +298,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -357,7 +330,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -392,7 +365,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -443,7 +416,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ @@ -493,7 +466,7 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification GraphQLSchema schema = mkSchema(sdl) def tree = createNormalizedTree(schema, query) when: - def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = compileToDocumentWithDeferSupport(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: printed == '''{ diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 99079c8705..8b35c67b09 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -1,6 +1,5 @@ package graphql.normalized -import com.google.common.collect.ImmutableListMultimap import graphql.GraphQL import graphql.TestUtil import graphql.execution.RawVariables @@ -12,7 +11,6 @@ import graphql.language.Field import graphql.language.IntValue import graphql.language.OperationDefinition import graphql.language.StringValue -import graphql.normalized.incremental.DeferExecution import graphql.parser.Parser import graphql.schema.GraphQLSchema import graphql.schema.idl.RuntimeWiring @@ -205,7 +203,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat def tree = createNormalizedTree(schema, query) when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -256,7 +254,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -337,7 +335,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -429,7 +427,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -524,7 +522,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -591,7 +589,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -648,7 +646,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -706,7 +704,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -773,7 +771,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -871,7 +869,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -969,7 +967,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat // printTreeWithLevelInfo(tree, schema).forEach { println it } when: - def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables, tree.normalizedFieldToDeferExecution) + def result = localCompileToDocument(schema, QUERY, null, tree.topLevelFields, noVariables) def printed = AstPrinter.printAst(new AstSorter().sort(result.document)) then: @@ -1285,7 +1283,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat when: - def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables, null) + def result = localCompileToDocument(schema, SUBSCRIPTION, null, eno.topLevelFields, eno.normalizedFieldToQueryDirectives, noVariables) OperationDefinition operationDefinition = result.document.getDefinitionsOfType(OperationDefinition.class)[0] def fooField = (Field) operationDefinition.selectionSet.children[0] def nameField = (Field) fooField.selectionSet.children[0] @@ -2175,18 +2173,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat List topLevelFields, VariablePredicate variablePredicate ) { - return localCompileToDocument(schema, operationKind, operationName, topLevelFields, variablePredicate, null) - } - - private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( - GraphQLSchema schema, - OperationDefinition.Operation operationKind, - String operationName, - List topLevelFields, - VariablePredicate variablePredicate, - Map> normalizedFieldToDeferExecution - ) { - return localCompileToDocument(schema, operationKind, operationName, topLevelFields, Map.of(), variablePredicate, normalizedFieldToDeferExecution) + return localCompileToDocument(schema, operationKind, operationName, topLevelFields,Map.of(), variablePredicate); } private static ExecutableNormalizedOperationToAstCompiler.CompilerResult localCompileToDocument( @@ -2195,11 +2182,10 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat String operationName, List topLevelFields, Map normalizedFieldToQueryDirectives, - VariablePredicate variablePredicate, - Map> normalizedFieldToDeferExecution + VariablePredicate variablePredicate ) { if (deferSupport) { - return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate, normalizedFieldToDeferExecution) + return compileToDocumentWithDeferSupport(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } return compileToDocument(schema, operationKind, operationName, topLevelFields, normalizedFieldToQueryDirectives, variablePredicate) } From b9b0f5497c9efba421050e58ac65db2740009b1a Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 13:03:01 +1100 Subject: [PATCH 22/33] WIP: attempt of getting rid of the NormalizedDeferExecutionFactory WIP WIP --- .../java/graphql/normalized/ENFMerger.java | 19 +++- .../normalized/ExecutableNormalizedField.java | 16 ++- .../ExecutableNormalizedOperationFactory.java | 106 ++++++++---------- ...tableNormalizedOperationToAstCompiler.java | 10 +- .../incremental/DeferDeclaration.java | 34 ++++-- .../incremental/IncrementalNodes.java | 10 +- .../NormalizedDeferExecutionFactory.java | 4 +- ...NormalizedOperationFactoryDeferTest.groovy | 56 +++++++-- 8 files changed, 160 insertions(+), 95 deletions(-) diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index d876cf56db..94d01ee739 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,11 +1,9 @@ package graphql.normalized; -import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; -import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -25,7 +23,7 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - Multimap normalizedFieldToDeferExecution + boolean deferSupport ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same @@ -74,9 +72,20 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); // Move defer executions from removed field into the merged field's entry - normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); +// normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); + if (deferSupport) { + first.getDeferExecutions().forEach(deferDeclarationFirst -> { + next.getDeferExecutions().forEach(deferDeclarationNext -> { + if (Objects.equals(deferDeclarationFirst.getLabel(), deferDeclarationNext.getLabel()) + && mergedObjects.containsAll(deferDeclarationNext.getObjectTypeNames()) + ) { + deferDeclarationFirst.addObjectTypes(deferDeclarationNext.getObjectTypes()); + } + }); + }); + } parent.getChildren().remove(next); - normalizedFieldToDeferExecution.removeAll(next); +// normalizedFieldToDeferExecution.removeAll(next); } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 93f128fa59..d27d8a970d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -11,6 +11,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; +import graphql.normalized.incremental.DeferDeclaration; import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; @@ -67,7 +68,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -263,11 +264,16 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } + @Internal + public void addDeferExecutions(Collection deferDeclarations) { + this.deferExecutions.addAll(deferDeclarations); + } + /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -475,7 +481,7 @@ public ExecutableNormalizedField getParent() { // TODO: Javadoc @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -606,7 +612,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -677,7 +683,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 0ec0891ab8..ed65c92de0 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -45,7 +45,6 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -60,7 +59,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -412,12 +410,12 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; - LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); - normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); +// LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); +// normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); - Consumer captureCollectNFResult = (collectNFResult -> { - normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); - }); +// Consumer captureCollectNFResult = (collectNFResult -> { +// normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); +// }); for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); @@ -437,17 +435,17 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), - captureCollectNFResult, +// captureCollectNFResult, options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.getDeferSupport()); } - if (options.deferSupport) { - NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); - } +// if (options.deferSupport) { +// NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); +// } return new ExecutableNormalizedOperation( operationDefinition.getOperation(), @@ -469,7 +467,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, - Consumer captureCollectNFResult, +// Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -477,7 +475,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); - captureCollectNFResult.accept(nextLevel); +// captureCollectNFResult.accept(nextLevel); for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); @@ -497,7 +495,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, - captureCollectNFResult, +// captureCollectNFResult, deferSupport); } } @@ -535,16 +533,16 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; - private final ImmutableSetMultimap normalizedFieldToDeferExecution; +// private final ImmutableSetMultimap normalizedFieldToDeferExecution; public CollectNFResult( Collection children, - ImmutableListMultimap normalizedFieldToAstFields, - ImmutableSetMultimap normalizedFieldToDeferExecution + ImmutableListMultimap normalizedFieldToAstFields +// ImmutableSetMultimap normalizedFieldToDeferExecution ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; - this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; +// this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -557,7 +555,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of());//, ImmutableSetMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -578,11 +576,11 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); +// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); +// + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build());//, normalizedFieldToDeferExecution.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -604,11 +602,11 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); +// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, @@ -623,7 +621,6 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -638,7 +635,8 @@ private void createNFs(ImmutableList.Builder nfListBu } nfListBuilder.add(nf); if (deferSupport) { - normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + nf.addDeferExecutions(fieldGroup.deferExecutions); +// normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); } } if (commonParentsGroups.size() > 1) { @@ -729,8 +727,20 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build().stream() + Set allDeferExecutions = deferExecutionsBuilder.build(); + + Set> typeSets = allDeferExecutions.stream().map(DeferDeclaration::getObjectTypeNames).collect(toSet()); + + Set deferExecutions = allDeferExecutions.stream() .filter(distinctByNullLabelAndType()) + .filter(deferDeclaration -> { + return typeSets.stream().noneMatch(typeSet -> { + boolean isSubset = typeSet.containsAll(deferDeclaration.getObjectTypeNames()); + boolean isSame = typeSet.equals(deferDeclaration.getObjectTypeNames()); + + return isSubset && !isSame; + }); + }) .collect(toCollection(LinkedHashSet::new)); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -750,7 +760,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); Set filteredDeferExecutions = deferExecutions.stream() - .filter(filter(objectType)) + .filter(deferExecution -> deferExecution.getObjectTypeNames().contains(objectType.getName())) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -758,21 +768,6 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filter(GraphQLObjectType objectType) { - return deferExecution -> { - if (deferExecution.getTargetType() == null) { - return true; - } - - if (deferExecution.getTargetType().equals(objectType.getName())) { - return true; - } - - return objectType.getInterfaces().stream() - .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); - }; - } - /** * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. @@ -796,16 +791,12 @@ private static Predicate filter(GraphQLObjectType objectType) * } * */ - private static @NotNull Predicate distinctByNullLabelAndType() { - Map seen = new ConcurrentHashMap<>(); + private static Predicate distinctByNullLabelAndType() { + Map, Boolean> seen = new ConcurrentHashMap<>(); return deferExecution -> { if (deferExecution.getLabel() == null) { - String typeName = Optional.ofNullable(deferExecution.getTargetType()) - .map(String::toUpperCase) - .orElse("null"); - - return seen.putIfAbsent(typeName, Boolean.TRUE) == null; + return seen.putIfAbsent(deferExecution.getObjectTypeNames(), Boolean.TRUE) == null; } return true; @@ -884,14 +875,15 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } + GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); + Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); + DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), fragmentSpread.getDirectives(), - fragmentDefinition.getTypeCondition() + newPossibleObjects ); - GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); - Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -916,7 +908,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), - inlineFragment.getTypeCondition() + newPossibleObjects ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index cae6f42ae7..ce7f1f4deb 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -279,7 +279,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -322,8 +322,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); + if (typeAndDeferPair.deferExecution.getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -491,9 +491,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final NormalizedDeferExecution deferExecution; + private final DeferDeclaration deferExecution; - public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, DeferDeclaration deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java index 688b9dd8a8..4db580f169 100644 --- a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java +++ b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java @@ -1,8 +1,15 @@ package graphql.normalized.incremental; +import graphql.Assert; import graphql.Internal; +import graphql.schema.GraphQLObjectType; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; /** * TODO: Javadoc @@ -10,11 +17,15 @@ @Internal public class DeferDeclaration { private final String label; - private final String targetType; - public DeferDeclaration(@Nullable String label, @Nullable String targetType) { + private final LinkedHashSet objectTypes; + + public DeferDeclaration(@Nullable String label, @Nonnull Set objectTypes) { this.label = label; - this.targetType = targetType; + + Assert.assertNotEmpty(objectTypes, () -> "A defer declaration must be associated with at least 1 GraphQL object"); + + this.objectTypes = new LinkedHashSet<>(objectTypes); } /** @@ -25,11 +36,20 @@ public String getLabel() { return label; } + public void addObjectTypes(Collection objectTypes) { + this.objectTypes.addAll(objectTypes); + } + /** - * @return the name of the type that is the target of the defer declaration + * TODO Javadoc */ - @Nullable - public String getTargetType() { - return targetType; + public Set getObjectTypes() { + return objectTypes; + } + + public Set getObjectTypeNames() { + return objectTypes.stream() + .map(GraphQLObjectType::getName) + .collect(Collectors.toSet()); } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 5b3f240d36..e433b2beeb 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -8,11 +8,13 @@ import graphql.language.Directive; import graphql.language.NodeUtil; import graphql.language.TypeName; +import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import static graphql.Directives.DeferDirective; @@ -22,7 +24,7 @@ public class IncrementalNodes { public DeferDeclaration getDeferExecution( Map variables, List directives, - @Nullable TypeName targetType + Set possibleTypes ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -38,15 +40,13 @@ public DeferDeclaration getDeferExecution( Object label = argumentValues.get("label"); - String targetTypeName = targetType == null ? null : targetType.getName(); - if (label == null) { - return new DeferDeclaration(null, targetTypeName); + return new DeferDeclaration(null, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferDeclaration((String) label, targetTypeName); + return new DeferDeclaration((String) label, possibleTypes); } diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java index 0ef085332b..59ab5e7cc2 100644 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -74,7 +74,7 @@ private void execute() { Set types = executionsForLabel.stream() .map(deferExecution -> { declarationToBlock.put(deferExecution, deferBlock); - return deferExecution.getTargetType(); + return "MOCK"; //deferExecution.getTargetType(); }) .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) .collect(Collectors.toSet()); @@ -86,7 +86,7 @@ private void execute() { if (!deferExecutions.isEmpty()) { // Mutate the field, by setting deferExecutions - field.setDeferExecutions(deferExecutions); +// field.setDeferExecutions(deferExecutions); } }); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index eb2260ee11..b46aa6318b 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -31,20 +31,26 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { name: String age: Int } + + interface HousePet implements Animal & LivingThing { + name: String + age: Int + owner: Person + } - type Dog implements Animal & LivingThing { + type Dog implements Animal & LivingThing & HousePet { name: String age: Int breed: String owner: Person } - type Cat implements Animal & LivingThing { + type Cat implements Animal & LivingThing & HousePet { name: String age: Int breed: String color: String - siblings: [Cat] + owner: Person } type Fish implements Animal & LivingThing { @@ -113,6 +119,37 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } + def "fragment on field from interface that reduces the possible object types"() { + given: + + String query = ''' + query q { + animal { + ... on HousePet @defer { + name + owner { + firstname + } + age + } + } + } + ''' + + Map variables = [:] + + when: + List printedTree = executeQueryAndPrintTree(query, variables) + + then: + printedTree == ['Query.animal', + "[Cat, Dog].name defer{[label=null;types=[Cat, Dog]]}", + "[Cat, Dog].owner defer{[label=null;types=[Cat, Dog]]}", + "Person.firstname", + "[Cat, Dog].age defer{[label=null;types=[Cat, Dog]]}" + ] + } + def "fragments on non-conditional fields"() { given: @@ -390,7 +427,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) - then: "should result in the same instance of defer" + then: "should result in different instances of defer" def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") @@ -400,8 +437,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { catBreedField.deferExecutions.size() == 1 // same label instances - nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock - dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock + nameField.deferExecutions[0] != dogBreedField.deferExecutions[0] + dogBreedField.deferExecutions[0] != catBreedField.deferExecutions[0] + nameField.deferExecutions[0] != catBreedField.deferExecutions[0] printedTree == ['Query.animal', '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', @@ -441,7 +479,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { breedField.deferExecutions.size() == 1 // different label instances - nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock + nameField.deferExecutions[0] != breedField.deferExecutions[0] printedTree == ['Query.dog', 'Dog.name defer{[label=null;types=[Dog]]}', @@ -884,8 +922,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = new ArrayList<>(deferExecutions) - .sort { it.deferBlock.label } - .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } + .sort { it.label } + .collect { "[label=${it.label};types=${it.objectTypeNames.sort()}]" } .join(",") return " defer{${deferLabels}}" From ca1f6d4d7289ee19a4e7db7ce95f4777bb6c1a4c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 14:17:28 +1100 Subject: [PATCH 23/33] Revert "WIP: attempt of getting rid of the NormalizedDeferExecutionFactory" This reverts commit b9b0f5497c9efba421050e58ac65db2740009b1a. --- .../java/graphql/normalized/ENFMerger.java | 19 +--- .../normalized/ExecutableNormalizedField.java | 16 +-- .../ExecutableNormalizedOperationFactory.java | 106 ++++++++++-------- ...tableNormalizedOperationToAstCompiler.java | 10 +- .../incremental/DeferDeclaration.java | 34 ++---- .../incremental/IncrementalNodes.java | 10 +- .../NormalizedDeferExecutionFactory.java | 4 +- ...NormalizedOperationFactoryDeferTest.groovy | 56 ++------- 8 files changed, 95 insertions(+), 160 deletions(-) diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index 94d01ee739..d876cf56db 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,9 +1,11 @@ package graphql.normalized; +import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; +import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -23,7 +25,7 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - boolean deferSupport + Multimap normalizedFieldToDeferExecution ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same @@ -72,20 +74,9 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); // Move defer executions from removed field into the merged field's entry -// normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); - if (deferSupport) { - first.getDeferExecutions().forEach(deferDeclarationFirst -> { - next.getDeferExecutions().forEach(deferDeclarationNext -> { - if (Objects.equals(deferDeclarationFirst.getLabel(), deferDeclarationNext.getLabel()) - && mergedObjects.containsAll(deferDeclarationNext.getObjectTypeNames()) - ) { - deferDeclarationFirst.addObjectTypes(deferDeclarationNext.getObjectTypes()); - } - }); - }); - } + normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); parent.getChildren().remove(next); -// normalizedFieldToDeferExecution.removeAll(next); + normalizedFieldToDeferExecution.removeAll(next); } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index d27d8a970d..93f128fa59 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -11,7 +11,6 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.DeferDeclaration; import graphql.normalized.incremental.NormalizedDeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; @@ -68,7 +67,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -264,16 +263,11 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } - @Internal - public void addDeferExecutions(Collection deferDeclarations) { - this.deferExecutions.addAll(deferDeclarations); - } - /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -481,7 +475,7 @@ public ExecutableNormalizedField getParent() { // TODO: Javadoc @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -612,7 +606,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -683,7 +677,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index ed65c92de0..0ec0891ab8 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -45,6 +45,7 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -59,6 +60,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -410,12 +412,12 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; -// LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); -// normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); + LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); + normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); -// Consumer captureCollectNFResult = (collectNFResult -> { -// normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); -// }); + Consumer captureCollectNFResult = (collectNFResult -> { + normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); + }); for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); @@ -435,17 +437,17 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), -// captureCollectNFResult, + captureCollectNFResult, options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.getDeferSupport()); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); } -// if (options.deferSupport) { -// NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); -// } + if (options.deferSupport) { + NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); + } return new ExecutableNormalizedOperation( operationDefinition.getOperation(), @@ -467,7 +469,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, -// Consumer captureCollectNFResult, + Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -475,7 +477,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); -// captureCollectNFResult.accept(nextLevel); + captureCollectNFResult.accept(nextLevel); for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); @@ -495,7 +497,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, -// captureCollectNFResult, + captureCollectNFResult, deferSupport); } } @@ -533,16 +535,16 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; -// private final ImmutableSetMultimap normalizedFieldToDeferExecution; + private final ImmutableSetMultimap normalizedFieldToDeferExecution; public CollectNFResult( Collection children, - ImmutableListMultimap normalizedFieldToAstFields -// ImmutableSetMultimap normalizedFieldToDeferExecution + ImmutableListMultimap normalizedFieldToAstFields, + ImmutableSetMultimap normalizedFieldToDeferExecution ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; -// this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; + this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -555,7 +557,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of());//, ImmutableSetMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -576,11 +578,11 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); -// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); -// - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); + + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build());//, normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -602,11 +604,11 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); -// ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); } public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, @@ -621,6 +623,7 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, + ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -635,8 +638,7 @@ private void createNFs(ImmutableList.Builder nfListBu } nfListBuilder.add(nf); if (deferSupport) { - nf.addDeferExecutions(fieldGroup.deferExecutions); -// normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); } } if (commonParentsGroups.size() > 1) { @@ -727,20 +729,8 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set allDeferExecutions = deferExecutionsBuilder.build(); - - Set> typeSets = allDeferExecutions.stream().map(DeferDeclaration::getObjectTypeNames).collect(toSet()); - - Set deferExecutions = allDeferExecutions.stream() + Set deferExecutions = deferExecutionsBuilder.build().stream() .filter(distinctByNullLabelAndType()) - .filter(deferDeclaration -> { - return typeSets.stream().noneMatch(typeSet -> { - boolean isSubset = typeSet.containsAll(deferDeclaration.getObjectTypeNames()); - boolean isSame = typeSet.equals(deferDeclaration.getObjectTypeNames()); - - return isSubset && !isSame; - }); - }) .collect(toCollection(LinkedHashSet::new)); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -760,7 +750,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); Set filteredDeferExecutions = deferExecutions.stream() - .filter(deferExecution -> deferExecution.getObjectTypeNames().contains(objectType.getName())) + .filter(filter(objectType)) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -768,6 +758,21 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } + private static Predicate filter(GraphQLObjectType objectType) { + return deferExecution -> { + if (deferExecution.getTargetType() == null) { + return true; + } + + if (deferExecution.getTargetType().equals(objectType.getName())) { + return true; + } + + return objectType.getInterfaces().stream() + .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); + }; + } + /** * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. @@ -791,12 +796,16 @@ private List groupByCommonParentsWithDeferSupport(Collectio * } * */ - private static Predicate distinctByNullLabelAndType() { - Map, Boolean> seen = new ConcurrentHashMap<>(); + private static @NotNull Predicate distinctByNullLabelAndType() { + Map seen = new ConcurrentHashMap<>(); return deferExecution -> { if (deferExecution.getLabel() == null) { - return seen.putIfAbsent(deferExecution.getObjectTypeNames(), Boolean.TRUE) == null; + String typeName = Optional.ofNullable(deferExecution.getTargetType()) + .map(String::toUpperCase) + .orElse("null"); + + return seen.putIfAbsent(typeName, Boolean.TRUE) == null; } return true; @@ -875,15 +884,14 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); - Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); - DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), fragmentSpread.getDirectives(), - newPossibleObjects + fragmentDefinition.getTypeCondition() ); + GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); + Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -908,7 +916,7 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), - newPossibleObjects + inlineFragment.getTypeCondition() ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index ce7f1f4deb..cae6f42ae7 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -279,7 +279,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -322,8 +322,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); + if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -491,9 +491,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final DeferDeclaration deferExecution; + private final NormalizedDeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, DeferDeclaration deferExecution) { + public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java index 4db580f169..688b9dd8a8 100644 --- a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java +++ b/src/main/java/graphql/normalized/incremental/DeferDeclaration.java @@ -1,15 +1,8 @@ package graphql.normalized.incremental; -import graphql.Assert; import graphql.Internal; -import graphql.schema.GraphQLObjectType; -import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.stream.Collectors; /** * TODO: Javadoc @@ -17,15 +10,11 @@ @Internal public class DeferDeclaration { private final String label; + private final String targetType; - private final LinkedHashSet objectTypes; - - public DeferDeclaration(@Nullable String label, @Nonnull Set objectTypes) { + public DeferDeclaration(@Nullable String label, @Nullable String targetType) { this.label = label; - - Assert.assertNotEmpty(objectTypes, () -> "A defer declaration must be associated with at least 1 GraphQL object"); - - this.objectTypes = new LinkedHashSet<>(objectTypes); + this.targetType = targetType; } /** @@ -36,20 +25,11 @@ public String getLabel() { return label; } - public void addObjectTypes(Collection objectTypes) { - this.objectTypes.addAll(objectTypes); - } - /** - * TODO Javadoc + * @return the name of the type that is the target of the defer declaration */ - public Set getObjectTypes() { - return objectTypes; - } - - public Set getObjectTypeNames() { - return objectTypes.stream() - .map(GraphQLObjectType::getName) - .collect(Collectors.toSet()); + @Nullable + public String getTargetType() { + return targetType; } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index e433b2beeb..5b3f240d36 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -8,13 +8,11 @@ import graphql.language.Directive; import graphql.language.NodeUtil; import graphql.language.TypeName; -import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import static graphql.Directives.DeferDirective; @@ -24,7 +22,7 @@ public class IncrementalNodes { public DeferDeclaration getDeferExecution( Map variables, List directives, - Set possibleTypes + @Nullable TypeName targetType ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -40,13 +38,15 @@ public DeferDeclaration getDeferExecution( Object label = argumentValues.get("label"); + String targetTypeName = targetType == null ? null : targetType.getName(); + if (label == null) { - return new DeferDeclaration(null, possibleTypes); + return new DeferDeclaration(null, targetTypeName); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferDeclaration((String) label, possibleTypes); + return new DeferDeclaration((String) label, targetTypeName); } diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java index 59ab5e7cc2..0ef085332b 100644 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -74,7 +74,7 @@ private void execute() { Set types = executionsForLabel.stream() .map(deferExecution -> { declarationToBlock.put(deferExecution, deferBlock); - return "MOCK"; //deferExecution.getTargetType(); + return deferExecution.getTargetType(); }) .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) .collect(Collectors.toSet()); @@ -86,7 +86,7 @@ private void execute() { if (!deferExecutions.isEmpty()) { // Mutate the field, by setting deferExecutions -// field.setDeferExecutions(deferExecutions); + field.setDeferExecutions(deferExecutions); } }); } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index b46aa6318b..eb2260ee11 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -31,26 +31,20 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { name: String age: Int } - - interface HousePet implements Animal & LivingThing { - name: String - age: Int - owner: Person - } - type Dog implements Animal & LivingThing & HousePet { + type Dog implements Animal & LivingThing { name: String age: Int breed: String owner: Person } - type Cat implements Animal & LivingThing & HousePet { + type Cat implements Animal & LivingThing { name: String age: Int breed: String color: String - owner: Person + siblings: [Cat] } type Fish implements Animal & LivingThing { @@ -119,37 +113,6 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "fragment on field from interface that reduces the possible object types"() { - given: - - String query = ''' - query q { - animal { - ... on HousePet @defer { - name - owner { - firstname - } - age - } - } - } - ''' - - Map variables = [:] - - when: - List printedTree = executeQueryAndPrintTree(query, variables) - - then: - printedTree == ['Query.animal', - "[Cat, Dog].name defer{[label=null;types=[Cat, Dog]]}", - "[Cat, Dog].owner defer{[label=null;types=[Cat, Dog]]}", - "Person.firstname", - "[Cat, Dog].age defer{[label=null;types=[Cat, Dog]]}" - ] - } - def "fragments on non-conditional fields"() { given: @@ -427,7 +390,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) - then: "should result in different instances of defer" + then: "should result in the same instance of defer" def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") @@ -437,9 +400,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { catBreedField.deferExecutions.size() == 1 // same label instances - nameField.deferExecutions[0] != dogBreedField.deferExecutions[0] - dogBreedField.deferExecutions[0] != catBreedField.deferExecutions[0] - nameField.deferExecutions[0] != catBreedField.deferExecutions[0] + nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock + dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock printedTree == ['Query.animal', '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', @@ -479,7 +441,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { breedField.deferExecutions.size() == 1 // different label instances - nameField.deferExecutions[0] != breedField.deferExecutions[0] + nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock printedTree == ['Query.dog', 'Dog.name defer{[label=null;types=[Dog]]}', @@ -922,8 +884,8 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = new ArrayList<>(deferExecutions) - .sort { it.label } - .collect { "[label=${it.label};types=${it.objectTypeNames.sort()}]" } + .sort { it.deferBlock.label } + .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } .join(",") return " defer{${deferLabels}}" From 6ba3e09f34d6b14a43e1418607728f299c31aefc Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 15:22:13 +1100 Subject: [PATCH 24/33] Remove excessive normalization that was resulting in reduced number of defer blocks --- .../java/graphql/normalized/ENFMerger.java | 10 ++- .../ExecutableNormalizedOperationFactory.java | 54 ++------------- .../NormalizedDeferExecutionFactory.java | 34 ++++----- ...NormalizedOperationFactoryDeferTest.groovy | 69 ++++++++++++++++--- ...izedOperationToAstCompilerDeferTest.groovy | 9 +++ 5 files changed, 95 insertions(+), 81 deletions(-) diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index d876cf56db..69d5faaefa 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -25,7 +25,8 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - Multimap normalizedFieldToDeferExecution + Multimap normalizedFieldToDeferExecution, + boolean deferSupport ) { // they have all the same result key // we can only merge the fields if they have the same field name + arguments + all children are the same @@ -74,9 +75,12 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); // Move defer executions from removed field into the merged field's entry - normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); parent.getChildren().remove(next); - normalizedFieldToDeferExecution.removeAll(next); + + if (deferSupport) { + normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); + normalizedFieldToDeferExecution.removeAll(next); + } } first.setObjectTypeNames(mergedObjects); } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 0ec0891ab8..754e6a5904 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -45,7 +45,6 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -56,9 +55,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -415,9 +412,9 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); - Consumer captureCollectNFResult = (collectNFResult -> { - normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution); - }); + Consumer captureCollectNFResult = (collectNFResult -> + normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution) + ); for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); @@ -442,7 +439,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution, options.deferSupport); } if (options.deferSupport) { @@ -729,9 +726,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build().stream() - .filter(distinctByNullLabelAndType()) - .collect(toCollection(LinkedHashSet::new)); + Set deferExecutions = deferExecutionsBuilder.build(); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -773,45 +768,6 @@ private static Predicate filter(GraphQLObjectType objectType) }; } - /** - * This predicate prevents us from having more than 1 defer execution with "null" label per each TypeName. - * This type of duplication is exactly what ENFs want to avoid, because they are irrelevant for execution time. - * For example, this query: - *

-     *     query example {
-     *        ... @defer {
-     *            name
-     *        }
-     *        ... @defer {
-     *            name
-     *        }
-     *     }
-     * 
- * should result on single ENF. Essentially: - *
-     *     query example {
-     *        ... @defer {
-     *            name
-     *        }
-     *     }
-     * 
- */ - private static @NotNull Predicate distinctByNullLabelAndType() { - Map seen = new ConcurrentHashMap<>(); - - return deferExecution -> { - if (deferExecution.getLabel() == null) { - String typeName = Optional.ofNullable(deferExecution.getTargetType()) - .map(String::toUpperCase) - .orElse("null"); - - return seen.putIfAbsent(typeName, Boolean.TRUE) == null; - } - - return true; - }; - } - private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() .map(DeferDeclaration::getLabel) diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java index 0ef085332b..ffa17e8cca 100644 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java @@ -14,7 +14,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -25,18 +24,20 @@ // TODO: Javadoc @Internal public class NormalizedDeferExecutionFactory { + private NormalizedDeferExecutionFactory() { + } public static void normalizeDeferExecutions( GraphQLSchema graphQLSchema, Multimap normalizedFieldDeferExecution ) { - new DeferExecutionMergerInner(graphQLSchema, normalizedFieldDeferExecution).execute(); + new DeferExecutionMergerImpl(graphQLSchema, normalizedFieldDeferExecution).execute(); } - private static class DeferExecutionMergerInner { + private static class DeferExecutionMergerImpl { private final GraphQLSchema graphQLSchema; private final Multimap input; - private DeferExecutionMergerInner( + private DeferExecutionMergerImpl( GraphQLSchema graphQLSchema, Multimap normalizedFieldToDeferExecution ) { @@ -62,25 +63,20 @@ private void execute() { .collect(Collectors.groupingBy(execution -> Optional.ofNullable(execution.getLabel()))); Set deferExecutions = executionsByLabel.keySet().stream() - .map(label -> { + .flatMap(label -> { List executionsForLabel = executionsByLabel.get(label); - DeferBlock deferBlock = executionsForLabel.stream() - .map(declarationToBlock::get) - .filter(Objects::nonNull) - .findFirst() - .orElse(new DeferBlock(label.orElse(null))); - - Set types = executionsForLabel.stream() - .map(deferExecution -> { - declarationToBlock.put(deferExecution, deferBlock); - return deferExecution.getTargetType(); - }) - .flatMap(mapToPossibleTypes(fieldTypeNames, fieldTypes)) - .collect(Collectors.toSet()); + return executionsForLabel.stream() + .map(deferDeclaration -> { + DeferBlock deferBlock = declarationToBlock.computeIfAbsent(deferDeclaration, + key -> new DeferBlock(label.orElse(null))); - return new NormalizedDeferExecution(deferBlock, types); + Set types = mapToPossibleTypes(fieldTypeNames, fieldTypes) + .apply(deferDeclaration.getTargetType()) + .collect(Collectors.toSet()); + return new NormalizedDeferExecution(deferBlock, types); + }); }) .collect(Collectors.toSet()); diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index eb2260ee11..0b3a1d29ea 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -139,7 +139,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}", + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]],[label=null;types=[Cat, Dog, Fish]]}", ] } @@ -169,7 +169,46 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.animal', - "[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog]]}", + "[Cat, Dog, Fish].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]]}", + ] + } + + def "fragments on non-conditional fields - andi"() { + given: + + + String query = ''' + query q { + dog { + ... @defer { + name + age + } + ... @defer { + age + } + } + } + ''' +// This should result in age being on its own deferBlock + Map variables = [:] + + when: + def executableNormalizedOperation = createExecutableNormalizedOperations(query, variables); + + List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) + + then: + + def nameField = findField(executableNormalizedOperation, "Dog", "name") + def ageField = findField(executableNormalizedOperation, "Dog", "age") + + nameField.deferExecutions.size() == 1 + ageField.deferExecutions.size() == 2 + + printedTree == ['Query.dog', + "Dog.name defer{[label=null;types=[Dog]]}", + "Dog.age defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}", ] } @@ -246,7 +285,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.mammal', - '[Dog, Cat].name defer{[label=null;types=[Cat, Dog]]}', + '[Dog, Cat].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]]}', 'Dog.breed defer{[label=null;types=[Dog]]}', 'Cat.breed defer{[label=null;types=[Cat]]}', ] @@ -390,21 +429,30 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { List printedTree = printTreeWithIncrementalExecutionDetails(executableNormalizedOperation) - then: "should result in the same instance of defer" + then: "should result in the same instance of defer block" def nameField = findField(executableNormalizedOperation,"[Cat, Dog, Fish]","name") def dogBreedField = findField(executableNormalizedOperation, "Dog", "breed") def catBreedField = findField(executableNormalizedOperation, "Cat", "breed") - nameField.deferExecutions.size() == 1 + nameField.deferExecutions.size() == 3 dogBreedField.deferExecutions.size() == 1 catBreedField.deferExecutions.size() == 1 - // same label instances - nameField.deferExecutions[0].deferBlock == dogBreedField.deferExecutions[0].deferBlock - dogBreedField.deferExecutions[0].deferBlock == catBreedField.deferExecutions[0].deferBlock + // nameField should share a defer block with each of the other fields + nameField.deferExecutions.any { + it.deferBlock == dogBreedField.deferExecutions[0].deferBlock + } + nameField.deferExecutions.any { + it.deferBlock == catBreedField.deferExecutions[0].deferBlock + } + // also, nameField should have a defer block that is not shared with any other field + nameField.deferExecutions.any { + it.deferBlock != dogBreedField.deferExecutions[0].deferBlock && + it.deferBlock != catBreedField.deferExecutions[0].deferBlock + } printedTree == ['Query.animal', - '[Cat, Dog, Fish].name defer{[label=null;types=[Cat, Dog, Fish]]}', + '[Cat, Dog, Fish].name defer{[label=null;types=[Cat]],[label=null;types=[Dog]],[label=null;types=[Cat, Dog, Fish]]}', 'Dog.breed defer{[label=null;types=[Dog]]}', 'Cat.breed defer{[label=null;types=[Cat]]}', ] @@ -589,7 +637,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { then: printedTree == ['Query.dog', - 'Dog.name defer{[label=null;types=[Dog]]}', + 'Dog.name defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}', ] } @@ -885,6 +933,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { def deferLabels = new ArrayList<>(deferExecutions) .sort { it.deferBlock.label } + .sort { it.objectTypeNames } .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } .join(",") diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy index 8d7f9e91a3..13928fd991 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerDeferTest.groovy @@ -177,6 +177,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { breed } + ... @defer { + breed + } } } ''' @@ -277,6 +280,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification ... @defer { name } + ... @defer { + name + } } } ''' @@ -421,6 +427,9 @@ class ExecutableNormalizedOperationToAstCompilerDeferTest extends Specification then: printed == '''{ animal { + ... @defer { + name + } ... @defer { name } From b71a9a41d1f04d58bd46a30f712adb526d13054b Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 16:16:52 +1100 Subject: [PATCH 25/33] Big refactor: capture defer executions directly in ENFs --- .../java/graphql/normalized/ENFMerger.java | 12 +- .../normalized/ExecutableNormalizedField.java | 17 +-- .../ExecutableNormalizedOperationFactory.java | 87 +++++-------- ...tableNormalizedOperationToAstCompiler.java | 14 +-- ...erDeclaration.java => DeferExecution.java} | 15 ++- .../incremental/IncrementalNodes.java | 12 +- .../incremental/NormalizedDeferExecution.java | 43 ------- .../NormalizedDeferExecutionFactory.java | 115 ------------------ ...NormalizedOperationFactoryDeferTest.groovy | 16 +-- 9 files changed, 80 insertions(+), 251 deletions(-) rename src/main/java/graphql/normalized/incremental/{DeferDeclaration.java => DeferExecution.java} (58%) delete mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java delete mode 100644 src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java diff --git a/src/main/java/graphql/normalized/ENFMerger.java b/src/main/java/graphql/normalized/ENFMerger.java index 69d5faaefa..f1fbc37b95 100644 --- a/src/main/java/graphql/normalized/ENFMerger.java +++ b/src/main/java/graphql/normalized/ENFMerger.java @@ -1,11 +1,9 @@ package graphql.normalized; -import com.google.common.collect.Multimap; import graphql.Internal; import graphql.introspection.Introspection; import graphql.language.Argument; import graphql.language.AstComparator; -import graphql.normalized.incremental.DeferDeclaration; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; @@ -25,7 +23,6 @@ public static void merge( ExecutableNormalizedField parent, List childrenWithSameResultKey, GraphQLSchema schema, - Multimap normalizedFieldToDeferExecution, boolean deferSupport ) { // they have all the same result key @@ -66,20 +63,19 @@ && isFieldInSharedInterface(field, fieldInGroup, schema) } boolean mergeable = areFieldSetsTheSame(listOfChildrenForGroup); if (mergeable) { + Set mergedObjects = new LinkedHashSet<>(); + groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); // patching the first one to contain more objects, remove all others Iterator iterator = groupOfFields.iterator(); ExecutableNormalizedField first = iterator.next(); - Set mergedObjects = new LinkedHashSet<>(); - groupOfFields.forEach(f -> mergedObjects.addAll(f.getObjectTypeNames())); while (iterator.hasNext()) { ExecutableNormalizedField next = iterator.next(); - // Move defer executions from removed field into the merged field's entry parent.getChildren().remove(next); if (deferSupport) { - normalizedFieldToDeferExecution.putAll(first, normalizedFieldToDeferExecution.get(next)); - normalizedFieldToDeferExecution.removeAll(next); + // Move defer executions from removed field into the merged field's entry + first.addDeferExecutions(next.getDeferExecutions()); } } first.setObjectTypeNames(mergedObjects); diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 93f128fa59..0dfbd9a892 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import graphql.Assert; import graphql.ExperimentalApi; import graphql.Internal; @@ -11,7 +10,7 @@ import graphql.collect.ImmutableKit; import graphql.introspection.Introspection; import graphql.language.Argument; -import graphql.normalized.incremental.NormalizedDeferExecution; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; @@ -67,7 +66,7 @@ public class ExecutableNormalizedField { private final int level; // Mutable List on purpose: it is modified after creation - private final LinkedHashSet deferExecutions; + private final LinkedHashSet deferExecutions; private ExecutableNormalizedField(Builder builder) { this.alias = builder.alias; @@ -263,11 +262,15 @@ public void clearChildren() { } @Internal - public void setDeferExecutions(Collection deferExecutions) { + public void setDeferExecutions(Collection deferExecutions) { this.deferExecutions.clear(); this.deferExecutions.addAll(deferExecutions); } + public void addDeferExecutions(Collection deferExecutions) { + this.deferExecutions.addAll(deferExecutions); + } + /** * All merged fields have the same name so this is the name of the {@link ExecutableNormalizedField}. *

@@ -475,7 +478,7 @@ public ExecutableNormalizedField getParent() { // TODO: Javadoc @ExperimentalApi - public LinkedHashSet getDeferExecutions() { + public LinkedHashSet getDeferExecutions() { return deferExecutions; } @@ -606,7 +609,7 @@ public static class Builder { private LinkedHashMap resolvedArguments = new LinkedHashMap<>(); private ImmutableList astArguments = ImmutableKit.emptyList(); - private LinkedHashSet deferExecutions = new LinkedHashSet<>(); + private LinkedHashSet deferExecutions = new LinkedHashSet<>(); private Builder() { } @@ -677,7 +680,7 @@ public Builder parent(ExecutableNormalizedField parent) { return this; } - public Builder deferExecutions(LinkedHashSet deferExecutions) { + public Builder deferExecutions(LinkedHashSet deferExecutions) { this.deferExecutions = deferExecutions; return this; } diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 754e6a5904..eba246ede9 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -4,8 +4,6 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.LinkedHashMultimap; import graphql.Assert; import graphql.ExperimentalApi; import graphql.GraphQLContext; @@ -30,9 +28,8 @@ import graphql.language.Selection; import graphql.language.SelectionSet; import graphql.language.VariableDefinition; -import graphql.normalized.incremental.DeferDeclaration; +import graphql.normalized.incremental.DeferExecution; import graphql.normalized.incremental.IncrementalNodes; -import graphql.normalized.incremental.NormalizedDeferExecutionFactory; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; @@ -57,7 +54,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -409,13 +405,6 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr normalizedFieldToMergedField.put(enf, mergedFld); }; - LinkedHashMultimap normalizedFieldToDeferExecution = LinkedHashMultimap.create(); - normalizedFieldToDeferExecution.putAll(collectFromOperationResult.normalizedFieldToDeferExecution); - - Consumer captureCollectNFResult = (collectNFResult -> - normalizedFieldToDeferExecution.putAll(collectNFResult.normalizedFieldToDeferExecution) - ); - for (ExecutableNormalizedField topLevel : collectFromOperationResult.children) { ImmutableList fieldAndAstParents = collectFromOperationResult.normalizedFieldToAstFields.get(topLevel); MergedField mergedField = newMergedField(fieldAndAstParents); @@ -434,16 +423,11 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl(GraphQLSchema gr coordinatesToNormalizedFields, 1, options.getMaxChildrenDepth(), - captureCollectNFResult, options.getDeferSupport()); } for (FieldCollectorNormalizedQueryParams.PossibleMerger possibleMerger : parameters.getPossibleMergerList()) { List childrenWithSameResultKey = possibleMerger.parent.getChildrenWithSameResultKey(possibleMerger.resultKey); - ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, normalizedFieldToDeferExecution, options.deferSupport); - } - - if (options.deferSupport) { - NormalizedDeferExecutionFactory.normalizeDeferExecutions(graphQLSchema, normalizedFieldToDeferExecution); + ENFMerger.merge(possibleMerger.parent, childrenWithSameResultKey, graphQLSchema, options.deferSupport); } return new ExecutableNormalizedOperation( @@ -466,7 +450,6 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz ImmutableListMultimap.Builder coordinatesToNormalizedFields, int curLevel, int maxLevel, - Consumer captureCollectNFResult, boolean deferSupport) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); @@ -474,8 +457,6 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz CollectNFResult nextLevel = collectFromMergedField(fieldCollectorNormalizedQueryParams, executableNormalizedField, fieldAndAstParents, curLevel + 1, deferSupport); - captureCollectNFResult.accept(nextLevel); - for (ExecutableNormalizedField childENF : nextLevel.children) { executableNormalizedField.addChild(childENF); ImmutableList childFieldAndAstParents = nextLevel.normalizedFieldToAstFields.get(childENF); @@ -494,7 +475,6 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz coordinatesToNormalizedFields, curLevel + 1, maxLevel, - captureCollectNFResult, deferSupport); } } @@ -532,16 +512,13 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { public static class CollectNFResult { private final Collection children; private final ImmutableListMultimap normalizedFieldToAstFields; - private final ImmutableSetMultimap normalizedFieldToDeferExecution; public CollectNFResult( Collection children, - ImmutableListMultimap normalizedFieldToAstFields, - ImmutableSetMultimap normalizedFieldToDeferExecution + ImmutableListMultimap normalizedFieldToAstFields ) { this.children = children; this.normalizedFieldToAstFields = normalizedFieldToAstFields; - this.normalizedFieldToDeferExecution = normalizedFieldToDeferExecution; } } @@ -554,7 +531,7 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam List fieldDefs = executableNormalizedField.getFieldDefinitions(parameters.getGraphQLSchema()); Set possibleObjects = resolvePossibleObjects(fieldDefs, parameters.getGraphQLSchema()); if (possibleObjects.isEmpty()) { - return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of(), ImmutableSetMultimap.of()); + return new CollectNFResult(ImmutableKit.emptyList(), ImmutableListMultimap.of()); } List collectedFields = new ArrayList<>(); @@ -575,11 +552,10 @@ public CollectNFResult collectFromMergedField(FieldCollectorNormalizedQueryParam Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, normalizedFieldToDeferExecution, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, level, executableNormalizedField, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } private Map> fieldsByResultKey(List collectedFields) { @@ -601,11 +577,10 @@ public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); ImmutableListMultimap.Builder normalizedFieldToAstFields = ImmutableListMultimap.builder(); - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution = ImmutableSetMultimap.builder(); - createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, normalizedFieldToDeferExecution, deferSupport); + createNFs(resultNFs, parameters, fieldsByName, normalizedFieldToAstFields, 1, null, deferSupport); - return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build(), normalizedFieldToDeferExecution.build()); + return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, @@ -620,7 +595,6 @@ private void createNFs(ImmutableList.Builder nfListBu ImmutableListMultimap.Builder normalizedFieldToAstFields, int level, ExecutableNormalizedField parent, - ImmutableSetMultimap.Builder normalizedFieldToDeferExecution, boolean deferSupport) { for (String resultKey : fieldsByName.keySet()) { List fieldsWithSameResultKey = fieldsByName.get(resultKey); @@ -635,7 +609,7 @@ private void createNFs(ImmutableList.Builder nfListBu } nfListBuilder.add(nf); if (deferSupport) { - normalizedFieldToDeferExecution.putAll(nf, fieldGroup.deferExecutions); + nf.addDeferExecutions(fieldGroup.deferExecutions); } } if (commonParentsGroups.size() > 1) { @@ -676,9 +650,9 @@ private ExecutableNormalizedField createNF(FieldCollectorNormalizedQueryParams p private static class CollectedFieldGroup { Set objectTypes; Set fields; - Set deferExecutions; + Set deferExecutions; - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { this.fields = fields; this.objectTypes = objectTypes; this.deferExecutions = deferExecutions; @@ -713,12 +687,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< private List groupByCommonParentsWithDeferSupport(Collection fields) { ImmutableSet.Builder objectTypes = ImmutableSet.builder(); - ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); + ImmutableSet.Builder deferExecutionsBuilder = ImmutableSet.builder(); for (CollectedField collectedField : fields) { objectTypes.addAll(collectedField.objectTypes); - DeferDeclaration collectedDeferExecution = collectedField.deferExecution; + DeferExecution collectedDeferExecution = collectedField.deferExecution; if (collectedDeferExecution != null) { deferExecutionsBuilder.add(collectedDeferExecution); @@ -726,7 +700,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio } Set allRelevantObjects = objectTypes.build(); - Set deferExecutions = deferExecutionsBuilder.build(); + Set deferExecutions = deferExecutionsBuilder.build(); Set duplicatedLabels = listDuplicatedLabels(deferExecutions); @@ -744,7 +718,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - Set filteredDeferExecutions = deferExecutions.stream() + Set filteredDeferExecutions = deferExecutions.stream() .filter(filter(objectType)) .collect(toCollection(LinkedHashSet::new)); @@ -753,7 +727,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filter(GraphQLObjectType objectType) { + private static Predicate filter(GraphQLObjectType objectType) { return deferExecution -> { if (deferExecution.getTargetType() == null) { return true; @@ -768,9 +742,9 @@ private static Predicate filter(GraphQLObjectType objectType) }; } - private Set listDuplicatedLabels(Collection deferExecutions) { + private Set listDuplicatedLabels(Collection deferExecutions) { return deferExecutions.stream() - .map(DeferDeclaration::getLabel) + .map(DeferExecution::getLabel) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() @@ -785,7 +759,7 @@ private void collectFromSelectionSet(FieldCollectorNormalizedQueryParams paramet List result, GraphQLCompositeType astTypeCondition, Set possibleObjects, - DeferDeclaration deferExecution + DeferExecution deferExecution ) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { @@ -802,9 +776,9 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; - DeferDeclaration deferExecution; + DeferExecution deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferDeclaration deferExecution) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; @@ -840,14 +814,16 @@ private void collectFragmentSpread(FieldCollectorNormalizedQueryParams parameter return; } - DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( - parameters.getCoercedVariableValues(), - fragmentSpread.getDirectives(), - fragmentDefinition.getTypeCondition() - ); GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(parameters.getGraphQLSchema().getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); + + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( + parameters.getCoercedVariableValues(), + fragmentSpread.getDirectives(), + fragmentDefinition.getTypeCondition(), + newPossibleObjects); + collectFromSelectionSet(parameters, fragmentDefinition.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } @@ -869,10 +845,11 @@ private void collectInlineFragment(FieldCollectorNormalizedQueryParams parameter newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition, parameters.getGraphQLSchema()); } - DeferDeclaration newDeferExecution = incrementalNodes.getDeferExecution( + DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( parameters.getCoercedVariableValues(), inlineFragment.getDirectives(), - inlineFragment.getTypeCondition() + inlineFragment.getTypeCondition(), + newPossibleObjects ); collectFromSelectionSet(parameters, inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); @@ -883,7 +860,7 @@ private void collectField(FieldCollectorNormalizedQueryParams parameters, Field field, Set possibleObjectTypes, GraphQLCompositeType astTypeCondition, - DeferDeclaration deferExecution + DeferExecution deferExecution ) { if (!conditionalNodes.shouldInclude(field, parameters.getCoercedVariableValues(), diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java index cae6f42ae7..051d4655de 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationToAstCompiler.java @@ -23,8 +23,7 @@ import graphql.language.StringValue; import graphql.language.TypeName; import graphql.language.Value; -import graphql.normalized.incremental.DeferDeclaration; -import graphql.normalized.incremental.NormalizedDeferExecution; +import graphql.normalized.incremental.DeferExecution; import graphql.schema.GraphQLCompositeType; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -34,7 +33,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -279,7 +277,7 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor Map> fieldsByFragmentDetails = new LinkedHashMap<>(); for (ExecutableNormalizedField nf : executableNormalizedFields) { - LinkedHashSet deferExecutions = nf.getDeferExecutions(); + LinkedHashSet deferExecutions = nf.getDeferExecutions(); if (nf.isConditional(schema)) { selectionForNormalizedField(schema, nf, normalizedFieldToQueryDirectives, variableAccumulator, true) @@ -322,8 +320,8 @@ private static List> subselectionsForNormalizedFieldWithDeferSuppor if (typeAndDeferPair.deferExecution != null) { Directive.Builder deferBuilder = Directive.newDirective().name(Directives.DeferDirective.getName()); - if (typeAndDeferPair.deferExecution.getDeferBlock().getLabel() != null) { - deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getDeferBlock().getLabel())).build()); + if (typeAndDeferPair.deferExecution.getLabel() != null) { + deferBuilder.argument(newArgument().name("label").value(StringValue.of(typeAndDeferPair.deferExecution.getLabel())).build()); } fragmentBuilder.directive(deferBuilder.build()); @@ -491,9 +489,9 @@ private static GraphQLObjectType getOperationType(@NotNull GraphQLSchema schema, */ private static class ExecutionFragmentDetails { private final String typeName; - private final NormalizedDeferExecution deferExecution; + private final DeferExecution deferExecution; - public ExecutionFragmentDetails(String typeName, NormalizedDeferExecution deferExecution) { + public ExecutionFragmentDetails(String typeName, DeferExecution deferExecution) { this.typeName = typeName; this.deferExecution = deferExecution; } diff --git a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java similarity index 58% rename from src/main/java/graphql/normalized/incremental/DeferDeclaration.java rename to src/main/java/graphql/normalized/incremental/DeferExecution.java index 688b9dd8a8..f4bab2634f 100644 --- a/src/main/java/graphql/normalized/incremental/DeferDeclaration.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -1,20 +1,24 @@ package graphql.normalized.incremental; import graphql.Internal; +import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; +import java.util.Set; /** * TODO: Javadoc */ @Internal -public class DeferDeclaration { +public class DeferExecution { private final String label; private final String targetType; + private final Set possibleTypes; - public DeferDeclaration(@Nullable String label, @Nullable String targetType) { + public DeferExecution(@Nullable String label, @Nullable String targetType, Set possibleTypes) { this.label = label; this.targetType = targetType; + this.possibleTypes = possibleTypes; } /** @@ -32,4 +36,11 @@ public String getLabel() { public String getTargetType() { return targetType; } + + /** + * TODO Javadoc + */ + public Set getPossibleTypes() { + return possibleTypes; + } } diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 5b3f240d36..47199f62a8 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -8,21 +8,24 @@ import graphql.language.Directive; import graphql.language.NodeUtil; import graphql.language.TypeName; +import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import static graphql.Directives.DeferDirective; @Internal public class IncrementalNodes { - public DeferDeclaration getDeferExecution( + public DeferExecution getDeferExecution( Map variables, List directives, - @Nullable TypeName targetType + @Nullable TypeName targetType, + Set possibleTypes ) { Directive deferDirective = NodeUtil.findNodeByName(directives, DeferDirective.getName()); @@ -41,13 +44,12 @@ public DeferDeclaration getDeferExecution( String targetTypeName = targetType == null ? null : targetType.getName(); if (label == null) { - return new DeferDeclaration(null, targetTypeName); + return new DeferExecution(null, targetTypeName, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferDeclaration((String) label, targetTypeName); - + return new DeferExecution((String) label, targetTypeName, possibleTypes); } return null; diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java deleted file mode 100644 index aad493018b..0000000000 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecution.java +++ /dev/null @@ -1,43 +0,0 @@ -package graphql.normalized.incremental; - -import graphql.ExperimentalApi; - -import javax.annotation.Nullable; -import java.util.Set; - -/** - * Represents the aspects of defer that are important for runtime execution. - */ -@ExperimentalApi -public class NormalizedDeferExecution { - private final DeferBlock deferBlock; - private final Set objectTypeNames; - - public NormalizedDeferExecution(DeferBlock deferBlock, Set objectTypeNames) { - this.deferBlock = deferBlock; - this.objectTypeNames = objectTypeNames; - } - - public Set getObjectTypeNames() { - return objectTypeNames; - } - - public DeferBlock getDeferBlock() { - return deferBlock; - } - - // TODO: Javadoc - @ExperimentalApi - public static class DeferBlock { - private final String label; - - public DeferBlock(@Nullable String label) { - this.label = label; - } - - @Nullable - public String getLabel() { - return label; - } - } -} diff --git a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java b/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java deleted file mode 100644 index ffa17e8cca..0000000000 --- a/src/main/java/graphql/normalized/incremental/NormalizedDeferExecutionFactory.java +++ /dev/null @@ -1,115 +0,0 @@ -package graphql.normalized.incremental; - -import com.google.common.collect.Multimap; -import graphql.Internal; -import graphql.normalized.ExecutableNormalizedField; -import graphql.normalized.incremental.NormalizedDeferExecution.DeferBlock; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLSchema; -import graphql.schema.GraphQLType; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -// TODO: Javadoc -@Internal -public class NormalizedDeferExecutionFactory { - private NormalizedDeferExecutionFactory() { - } - public static void normalizeDeferExecutions( - GraphQLSchema graphQLSchema, - Multimap normalizedFieldDeferExecution - ) { - new DeferExecutionMergerImpl(graphQLSchema, normalizedFieldDeferExecution).execute(); - } - - private static class DeferExecutionMergerImpl { - private final GraphQLSchema graphQLSchema; - private final Multimap input; - - private DeferExecutionMergerImpl( - GraphQLSchema graphQLSchema, - Multimap normalizedFieldToDeferExecution - ) { - this.graphQLSchema = graphQLSchema; - this.input = normalizedFieldToDeferExecution; - } - - private void execute() { - Map declarationToBlock = new HashMap<>(); - - this.input.keySet().forEach(field -> { - Collection executionsForField = input.get(field); - - Set fieldTypes = field.getObjectTypeNames().stream() - .map(graphQLSchema::getType) - .filter(GraphQLObjectType.class::isInstance) - .map(GraphQLObjectType.class::cast) - .collect(Collectors.toSet()); - - Set fieldTypeNames = fieldTypes.stream().map(GraphQLObjectType::getName).collect(Collectors.toSet()); - - Map, List> executionsByLabel = executionsForField.stream() - .collect(Collectors.groupingBy(execution -> Optional.ofNullable(execution.getLabel()))); - - Set deferExecutions = executionsByLabel.keySet().stream() - .flatMap(label -> { - List executionsForLabel = executionsByLabel.get(label); - - return executionsForLabel.stream() - .map(deferDeclaration -> { - DeferBlock deferBlock = declarationToBlock.computeIfAbsent(deferDeclaration, - key -> new DeferBlock(label.orElse(null))); - - Set types = mapToPossibleTypes(fieldTypeNames, fieldTypes) - .apply(deferDeclaration.getTargetType()) - .collect(Collectors.toSet()); - - return new NormalizedDeferExecution(deferBlock, types); - }); - }) - .collect(Collectors.toSet()); - - if (!deferExecutions.isEmpty()) { - // Mutate the field, by setting deferExecutions - field.setDeferExecutions(deferExecutions); - } - }); - } - - @NotNull - private Function> mapToPossibleTypes(Set fieldTypeNames, Set fieldTypes) { - return typeName -> { - if (typeName == null) { - return fieldTypeNames.stream(); - } - - GraphQLType type = graphQLSchema.getType(typeName); - - if (type instanceof GraphQLInterfaceType) { - return fieldTypes.stream() - .filter(filterImplementsInterface((GraphQLInterfaceType) type)) - .map(GraphQLObjectType::getName); - } - - return Stream.of(typeName); - }; - } - - private static Predicate filterImplementsInterface(GraphQLInterfaceType interfaceType) { - return objectType -> objectType.getInterfaces().stream() - .anyMatch(implementedInterface -> implementedInterface.getName().equals(interfaceType.getName())); - } - } - -} diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 0b3a1d29ea..95403c86d8 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -440,15 +440,15 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { // nameField should share a defer block with each of the other fields nameField.deferExecutions.any { - it.deferBlock == dogBreedField.deferExecutions[0].deferBlock + it == dogBreedField.deferExecutions[0] } nameField.deferExecutions.any { - it.deferBlock == catBreedField.deferExecutions[0].deferBlock + it == catBreedField.deferExecutions[0] } // also, nameField should have a defer block that is not shared with any other field nameField.deferExecutions.any { - it.deferBlock != dogBreedField.deferExecutions[0].deferBlock && - it.deferBlock != catBreedField.deferExecutions[0].deferBlock + it != dogBreedField.deferExecutions[0] && + it != catBreedField.deferExecutions[0] } printedTree == ['Query.animal', @@ -489,7 +489,7 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { breedField.deferExecutions.size() == 1 // different label instances - nameField.deferExecutions[0].deferBlock != breedField.deferExecutions[0].deferBlock + nameField.deferExecutions[0] != breedField.deferExecutions[0] printedTree == ['Query.dog', 'Dog.name defer{[label=null;types=[Dog]]}', @@ -932,9 +932,9 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { } def deferLabels = new ArrayList<>(deferExecutions) - .sort { it.deferBlock.label } - .sort { it.objectTypeNames } - .collect { "[label=${it.deferBlock.label};types=${it.objectTypeNames.sort()}]" } + .sort { it.label } + .sort { it.possibleTypes.collect {it.name} } + .collect { "[label=${it.label};types=${it.possibleTypes.collect{it.name}.sort()}]" } .join(",") return " defer{${deferLabels}}" From eda84d3ed4c65b263fb7e51dfa2cdf1ad3273265 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:28:13 +1100 Subject: [PATCH 26/33] post merge adjustments --- .../ExecutableNormalizedOperationFactory.java | 49 ++++++++-------- ...tableNormalizedOperationFactoryTest.groovy | 56 +++++++++---------- ...ormalizedOperationToAstCompilerTest.groovy | 2 +- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 86b074aba4..651cb22e20 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -38,6 +38,7 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; @@ -52,7 +53,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -242,14 +242,14 @@ public static ExecutableNormalizedOperation createExecutableNormalizedOperation( ) { NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operationName); - return createExecutableNormalizedOperation( + return new ExecutableNormalizedOperationFactoryImpl( graphQLSchema, getOperationResult.operationDefinition, getOperationResult.fragmentsByName, coercedVariableValues, null, options - ); + ).createNormalizedQueryImpl(); } /** @@ -425,8 +425,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { topLevel, fieldAndAstParents, 1, - options.getMaxChildrenDepth(), - options.deferSupport); + options.getMaxChildrenDepth()); } // getPossibleMergerList for (PossibleMerger possibleMerger : possibleMergerList) { @@ -444,7 +443,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { ); } - private void captureMergedField(ExecutableNormalizedField enf, MergedField mergedFld) { + private void captureMergedField(ExecutableNormalizedField enf, MergedField mergedFld) { // QueryDirectivesImpl is a lazy object and only computes itself when asked for QueryDirectives queryDirectives = new QueryDirectivesImpl(mergedFld, graphQLSchema, coercedVariableValues.toMap(), options.getGraphQLContext(), options.getLocale()); normalizedFieldToQueryDirectives.put(enf, queryDirectives); @@ -454,8 +453,7 @@ private void captureMergedField(ExecutableNormalizedField enf, MergedField merge private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, ImmutableList fieldAndAstParents, int curLevel, - int maxLevel, - boolean deferSupport) { + int maxLevel) { if (curLevel > maxLevel) { throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); } @@ -475,8 +473,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz buildFieldWithChildren(childENF, childFieldAndAstParents, curLevel + 1, - maxLevel, - deferSupport); + maxLevel); } } @@ -517,7 +514,8 @@ public CollectNFResult collectFromMergedField(ExecutableNormalizedField executab this.collectFromSelectionSet(fieldAndAstParent.field.getSelectionSet(), collectedFields, (GraphQLCompositeType) astParentType, - possibleObjects + possibleObjects, + null ); } Map> fieldsByName = fieldsByResultKey(collectedFields); @@ -542,7 +540,7 @@ public CollectNFResult collectFromOperation(GraphQLObjectType rootType) { Set possibleObjects = ImmutableSet.of(rootType); List collectedFields = new ArrayList<>(); - collectFromSelectionSet(operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects); + collectFromSelectionSet(operationDefinition.getSelectionSet(), collectedFields, rootType, possibleObjects, null); // group by result key Map> fieldsByName = fieldsByResultKey(collectedFields); ImmutableList.Builder resultNFs = ImmutableList.builder(); @@ -553,12 +551,6 @@ public CollectNFResult collectFromOperation(GraphQLObjectType rootType) { return new CollectNFResult(resultNFs.build(), normalizedFieldToAstFields.build()); } - public CollectNFResult collectFromOperation(FieldCollectorNormalizedQueryParams parameters, - OperationDefinition operationDefinition, - GraphQLObjectType rootType) { - return this.collectFromOperation(parameters, operationDefinition, rootType, false); - } - private void createNFs(ImmutableList.Builder nfListBuilder, Map> fieldsByName, ImmutableListMultimap.Builder normalizedFieldToAstFields, @@ -626,10 +618,9 @@ public CollectedFieldGroup(Set fields, Set ob this.deferExecutions = deferExecutions; } } - } - private List groupByCommonParents(Collection fields, boolean deferSupport) { - if (deferSupport) { + private List groupByCommonParents(Collection fields) { + if (this.options.deferSupport) { return groupByCommonParentsWithDeferSupport(fields); } else { return groupByCommonParentsNoDeferSupport(fields); @@ -644,12 +635,12 @@ private List groupByCommonParentsNoDeferSupport(Collection< Set allRelevantObjects = objectTypes.build(); Map> groupByAstParent = groupingBy(fields, fieldAndType -> fieldAndType.astTypeCondition); if (groupByAstParent.size() == 1) { - return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects)); + return singletonList(new CollectedFieldGroup(ImmutableSet.copyOf(fields), allRelevantObjects, null)); } ImmutableList.Builder result = ImmutableList.builder(); for (GraphQLObjectType objectType : allRelevantObjects) { Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); - result.add(new CollectedFieldGroup(relevantFields, singleton(objectType))); + result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), null)); } return result.build(); } @@ -868,11 +859,21 @@ private static class CollectedField { Field field; Set objectTypes; GraphQLCompositeType astTypeCondition; + DeferExecution deferExecution; - public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition) { + public CollectedField(Field field, Set objectTypes, GraphQLCompositeType astTypeCondition, DeferExecution deferExecution) { this.field = field; this.objectTypes = objectTypes; this.astTypeCondition = astTypeCondition; + this.deferExecution = deferExecution; + } + + public boolean isAbstract() { + return GraphQLTypeUtil.isInterfaceOrUnion(astTypeCondition); + } + + public boolean isConcrete() { + return GraphQLTypeUtil.isObjectType(astTypeCondition); } } diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy index 425542aab1..6063e7e448 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryTest.groovy @@ -329,7 +329,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -372,7 +372,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -422,7 +422,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -485,7 +485,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -531,7 +531,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -575,7 +575,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -619,7 +619,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -651,7 +651,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -702,7 +702,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -752,7 +752,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -791,7 +791,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -835,7 +835,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -875,7 +875,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -923,7 +923,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1026,7 +1026,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) def subFooField = (document.getDefinitions()[1] as FragmentDefinition).getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1069,7 +1069,7 @@ type Dog implements Animal{ def petsField = (document.getDefinitions()[0] as OperationDefinition).getSelectionSet().getSelections()[0] as Field def idField = petsField.getSelectionSet().getSelections()[0] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1118,7 +1118,7 @@ type Dog implements Animal{ def schemaField = selections[2] as Field def typeField = selections[3] as Field - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def fieldToNormalizedField = tree.getFieldToNormalizedField() @@ -1175,7 +1175,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -1218,7 +1218,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTree(tree) @@ -1246,7 +1246,7 @@ type Dog implements Animal{ Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def normalizedFieldToMergedField = tree.getNormalizedFieldToMergedField() Traverser traverser = Traverser.depthFirst({ it.getChildren() }) @@ -1385,7 +1385,7 @@ schema { Document document = TestUtil.parseQuery(mutation) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2584,7 +2584,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2637,7 +2637,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2684,7 +2684,7 @@ fragment personName on Person { Document document = TestUtil.parseQuery(query) - def dependencyGraph = new ExecutableNormalizedOperationFactory() + def tree = localCreateExecutableNormalizedOperation(graphQLSchema, document, null, CoercedVariables.emptyVariables()) def printedTree = printTreeWithLevelInfo(tree, graphQLSchema) @@ -2882,10 +2882,10 @@ fragment personName on Person { String operationName, CoercedVariables coercedVariableValues ) { - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return dependencyGraph.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperation(graphQLSchema, document, operationName, coercedVariableValues, options) } private static ExecutableNormalizedOperation localCreateExecutableNormalizedOperationWithRawVariables( @@ -2894,10 +2894,10 @@ fragment personName on Person { String operationName, RawVariables rawVariables ) { - ExecutableNormalizedOperationFactory dependencyGraph = new ExecutableNormalizedOperationFactory() + def options = ExecutableNormalizedOperationFactory.Options.defaultOptions().deferSupport(deferSupport) - return dependencyGraph.createExecutableNormalizedOperationWithRawVariables( + return ExecutableNormalizedOperationFactory.createExecutableNormalizedOperationWithRawVariables( graphQLSchema, document, operationName, diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy index 6c6b3e3b6b..27c4c89a6d 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationToAstCompilerTest.groovy @@ -2139,7 +2139,7 @@ abstract class ExecutableNormalizedOperationToAstCompilerTest extends Specificat v3: [arg1: "barFragArg"], v4: [arg1: "barArg"], v5: [arg1: "fooArg"]] - + } private ExecutableNormalizedOperation createNormalizedTree(GraphQLSchema schema, String query, Map variables = [:]) { assertValidQuery(schema, query, variables) From 87a5b52175fb0459fd590207557bfe2bf4ac961c Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:31:23 +1100 Subject: [PATCH 27/33] Remove irrelevant parameter --- .../ExecutableNormalizedOperationFactory.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 651cb22e20..cae62d5198 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -424,8 +424,7 @@ private ExecutableNormalizedOperation createNormalizedQueryImpl() { buildFieldWithChildren( topLevel, fieldAndAstParents, - 1, - options.getMaxChildrenDepth()); + 1); } // getPossibleMergerList for (PossibleMerger possibleMerger : possibleMergerList) { @@ -452,10 +451,9 @@ private void captureMergedField(ExecutableNormalizedField enf, MergedField merge private void buildFieldWithChildren(ExecutableNormalizedField executableNormalizedField, ImmutableList fieldAndAstParents, - int curLevel, - int maxLevel) { - if (curLevel > maxLevel) { - throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + maxLevel); + int curLevel) { + if (curLevel > this.options.getMaxChildrenDepth()) { + throw new AbortExecutionException("Maximum query depth exceeded " + curLevel + " > " + this.options.getMaxChildrenDepth()); } CollectNFResult nextLevel = collectFromMergedField(executableNormalizedField, fieldAndAstParents, curLevel + 1); @@ -472,8 +470,7 @@ private void buildFieldWithChildren(ExecutableNormalizedField executableNormaliz buildFieldWithChildren(childENF, childFieldAndAstParents, - curLevel + 1, - maxLevel); + curLevel + 1); } } From 5943b7c067fa58e66956b57e4f32d376163befb0 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:40:01 +1100 Subject: [PATCH 28/33] Minor refactors --- .../ExecutableNormalizedOperationFactory.java | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index cae62d5198..9eafaada50 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -18,6 +18,7 @@ import graphql.execution.directives.QueryDirectives; import graphql.execution.directives.QueryDirectivesImpl; import graphql.introspection.Introspection; +import graphql.language.Directive; import graphql.language.Document; import graphql.language.Field; import graphql.language.FragmentDefinition; @@ -27,6 +28,7 @@ import graphql.language.OperationDefinition; import graphql.language.Selection; import graphql.language.SelectionSet; +import graphql.language.TypeName; import graphql.language.VariableDefinition; import graphql.normalized.incremental.DeferExecution; import graphql.normalized.incremental.IncrementalNodes; @@ -604,18 +606,6 @@ private ExecutableNormalizedField createNF(CollectedFieldGroup collectedFieldGro .build(); } - private static class CollectedFieldGroup { - Set objectTypes; - Set fields; - Set deferExecutions; - - public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { - this.fields = fields; - this.objectTypes = objectTypes; - this.deferExecutions = deferExecutions; - } - } - private List groupByCommonParents(Collection fields) { if (this.options.deferSupport) { return groupByCommonParentsWithDeferSupport(fields); @@ -749,8 +739,7 @@ private void collectFragmentSpread(List result, GraphQLCompositeType newAstTypeCondition = (GraphQLCompositeType) assertNotNull(this.graphQLSchema.getType(fragmentDefinition.getTypeCondition().getName())); Set newPossibleObjects = narrowDownPossibleObjects(possibleObjects, newAstTypeCondition); - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( - this.coercedVariableValues.toMap(), + DeferExecution newDeferExecution = buildDeferExecution( fragmentSpread.getDirectives(), fragmentDefinition.getTypeCondition(), newPossibleObjects); @@ -775,8 +764,7 @@ private void collectInlineFragment(List result, } - DeferExecution newDeferExecution = incrementalNodes.getDeferExecution( - this.coercedVariableValues.toMap(), + DeferExecution newDeferExecution = buildDeferExecution( inlineFragment.getDirectives(), inlineFragment.getTypeCondition(), newPossibleObjects @@ -785,6 +773,22 @@ private void collectInlineFragment(List result, collectFromSelectionSet(inlineFragment.getSelectionSet(), result, newAstTypeCondition, newPossibleObjects, newDeferExecution); } + private DeferExecution buildDeferExecution( + List directives, + TypeName typeCondition, + Set newPossibleObjects) { + if(!options.deferSupport) { + return null; + } + + return incrementalNodes.getDeferExecution( + this.coercedVariableValues.toMap(), + directives, + typeCondition, + newPossibleObjects + ); + } + private void collectField(List result, Field field, Set possibleObjectTypes, @@ -893,6 +897,18 @@ private FieldAndAstParent(Field field, GraphQLCompositeType astParentType) { this.astParentType = astParentType; } } + + private static class CollectedFieldGroup { + Set objectTypes; + Set fields; + Set deferExecutions; + + public CollectedFieldGroup(Set fields, Set objectTypes, Set deferExecutions) { + this.fields = fields; + this.objectTypes = objectTypes; + this.deferExecutions = deferExecutions; + } + } } } From b04eadc32ab956cb8bcd3669c9f2eaa15e9e7848 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:48:14 +1100 Subject: [PATCH 29/33] Rename predicate function --- .../ExecutableNormalizedOperationFactory.java | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 9eafaada50..7116693d29 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -40,7 +40,6 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLUnionType; import graphql.schema.GraphQLUnmodifiedType; import graphql.schema.impl.SchemaUtil; @@ -113,7 +112,7 @@ public static Options defaultOptions() { * @return new options object to use */ public Options locale(Locale locale) { - return new Options(this.graphQLContext, locale, this.maxChildrenDepth, true); + return new Options(this.graphQLContext, locale, this.maxChildrenDepth, this.deferSupport); } /** @@ -126,7 +125,7 @@ public Options locale(Locale locale) { * @return new options object to use */ public Options graphQLContext(GraphQLContext graphQLContext) { - return new Options(graphQLContext, this.locale, this.maxChildrenDepth, true); + return new Options(graphQLContext, this.locale, this.maxChildrenDepth, this.deferSupport); } /** @@ -138,7 +137,7 @@ public Options graphQLContext(GraphQLContext graphQLContext) { * @return new options object to use */ public Options maxChildrenDepth(int maxChildrenDepth) { - return new Options(this.graphQLContext, this.locale, maxChildrenDepth, true); + return new Options(this.graphQLContext, this.locale, maxChildrenDepth, this.deferSupport); } /** @@ -666,7 +665,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio Set relevantFields = filterSet(fields, field -> field.objectTypes.contains(objectType)); Set filteredDeferExecutions = deferExecutions.stream() - .filter(filter(objectType)) + .filter(filterExecutionsFromType(objectType)) .collect(toCollection(LinkedHashSet::new)); result.add(new CollectedFieldGroup(relevantFields, singleton(objectType), filteredDeferExecutions)); @@ -674,7 +673,7 @@ private List groupByCommonParentsWithDeferSupport(Collectio return result.build(); } - private static Predicate filter(GraphQLObjectType objectType) { + private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { return deferExecution -> { if (deferExecution.getTargetType() == null) { return true; @@ -868,14 +867,6 @@ public CollectedField(Field field, Set objectTypes, GraphQLCo this.astTypeCondition = astTypeCondition; this.deferExecution = deferExecution; } - - public boolean isAbstract() { - return GraphQLTypeUtil.isInterfaceOrUnion(astTypeCondition); - } - - public boolean isConcrete() { - return GraphQLTypeUtil.isObjectType(astTypeCondition); - } } public static class CollectNFResult { From f50013215ca71ce2b12968def11541c35d3e239d Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Tue, 9 Jan 2024 17:53:57 +1100 Subject: [PATCH 30/33] Rename test and add better documentation --- ...NormalizedOperationFactoryDeferTest.groovy | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy index 95403c86d8..08ead8f5e2 100644 --- a/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy +++ b/src/test/groovy/graphql/normalized/ExecutableNormalizedOperationFactoryDeferTest.groovy @@ -173,24 +173,21 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { ] } - def "fragments on non-conditional fields - andi"() { + def "field on multiple defer declarations is associated with "() { given: - - String query = ''' query q { - dog { - ... @defer { - name - age - } - ... @defer { - age - } - } - } + dog { + ... @defer { + name + age + } + ... @defer { + age + } + } + } ''' -// This should result in age being on its own deferBlock Map variables = [:] when: @@ -206,6 +203,15 @@ class ExecutableNormalizedOperationFactoryDeferTest extends Specification { nameField.deferExecutions.size() == 1 ageField.deferExecutions.size() == 2 + // age field is associated with 2 defer executions, one of then is shared with "name" the other isn't + ageField.deferExecutions.any { + it == nameField.deferExecutions[0] + } + + ageField.deferExecutions.any { + it != nameField.deferExecutions[0] + } + printedTree == ['Query.dog', "Dog.name defer{[label=null;types=[Dog]]}", "Dog.age defer{[label=null;types=[Dog]],[label=null;types=[Dog]]}", From f3e5c846333c6097be3c0b2c2708721108d0ddef Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 10 Jan 2024 14:02:40 +1100 Subject: [PATCH 31/33] Small changes after Andi's review --- .../ExecutableNormalizedOperationFactory.java | 19 ++++++------------- .../incremental/DeferExecution.java | 12 +----------- .../incremental/IncrementalNodes.java | 8 +++----- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java index 7116693d29..9367c1d58d 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java @@ -674,18 +674,11 @@ private List groupByCommonParentsWithDeferSupport(Collectio } private static Predicate filterExecutionsFromType(GraphQLObjectType objectType) { - return deferExecution -> { - if (deferExecution.getTargetType() == null) { - return true; - } - - if (deferExecution.getTargetType().equals(objectType.getName())) { - return true; - } - - return objectType.getInterfaces().stream() - .anyMatch(inter -> inter.getName().equals(deferExecution.getTargetType())); - }; + String objectTypeName = objectType.getName(); + return deferExecution -> deferExecution.getPossibleTypes() + .stream() + .map(GraphQLObjectType::getName) + .anyMatch(objectTypeName::equals); } private Set listDuplicatedLabels(Collection deferExecutions) { @@ -780,7 +773,7 @@ private DeferExecution buildDeferExecution( return null; } - return incrementalNodes.getDeferExecution( + return incrementalNodes.createDeferExecution( this.coercedVariableValues.toMap(), directives, typeCondition, diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index f4bab2634f..14213ea480 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -12,12 +12,10 @@ @Internal public class DeferExecution { private final String label; - private final String targetType; private final Set possibleTypes; - public DeferExecution(@Nullable String label, @Nullable String targetType, Set possibleTypes) { + public DeferExecution(@Nullable String label, Set possibleTypes) { this.label = label; - this.targetType = targetType; this.possibleTypes = possibleTypes; } @@ -29,14 +27,6 @@ public String getLabel() { return label; } - /** - * @return the name of the type that is the target of the defer declaration - */ - @Nullable - public String getTargetType() { - return targetType; - } - /** * TODO Javadoc */ diff --git a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java index 47199f62a8..025b9333a0 100644 --- a/src/main/java/graphql/normalized/incremental/IncrementalNodes.java +++ b/src/main/java/graphql/normalized/incremental/IncrementalNodes.java @@ -21,7 +21,7 @@ @Internal public class IncrementalNodes { - public DeferExecution getDeferExecution( + public DeferExecution createDeferExecution( Map variables, List directives, @Nullable TypeName targetType, @@ -41,15 +41,13 @@ public DeferExecution getDeferExecution( Object label = argumentValues.get("label"); - String targetTypeName = targetType == null ? null : targetType.getName(); - if (label == null) { - return new DeferExecution(null, targetTypeName, possibleTypes); + return new DeferExecution(null, possibleTypes); } Assert.assertTrue(label instanceof String, () -> String.format("The 'label' argument from the '%s' directive MUST contain a String value", DeferDirective.getName())); - return new DeferExecution((String) label, targetTypeName, possibleTypes); + return new DeferExecution((String) label, possibleTypes); } return null; From a91fa642d98185f706650d14a9b315ef59fdeca8 Mon Sep 17 00:00:00 2001 From: Felipe Reis Date: Wed, 10 Jan 2024 14:35:14 +1100 Subject: [PATCH 32/33] Add Javadoc for DeferExecution --- .../normalized/ExecutableNormalizedField.java | 5 +- .../incremental/DeferExecution.java | 97 ++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/normalized/ExecutableNormalizedField.java b/src/main/java/graphql/normalized/ExecutableNormalizedField.java index 0dfbd9a892..3a8d36af08 100644 --- a/src/main/java/graphql/normalized/ExecutableNormalizedField.java +++ b/src/main/java/graphql/normalized/ExecutableNormalizedField.java @@ -476,7 +476,10 @@ public ExecutableNormalizedField getParent() { return parent; } - // TODO: Javadoc + /** + * @return the {@link DeferExecution}s associated with this {@link ExecutableNormalizedField}. + * @see DeferExecution + */ @ExperimentalApi public LinkedHashSet getDeferExecutions() { return deferExecutions; diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 14213ea480..0f2593c2b2 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -7,7 +7,100 @@ import java.util.Set; /** - * TODO: Javadoc + * Represents details about the defer execution that can be associated with a {@link graphql.normalized.ExecutableNormalizedField}. + * + * Taking this schema as an example: + *

+ *     type Query { animal: Animal }
+ *     interface Animal { name: String, age: Int }
+ *     type Cat implements Animal { name: String, age: Int }
+ *     type Dog implements Animal { name: String, age: Int }
+ * 
+ * + * An ENF can be associated with multiple `DeferExecution`s + * + * For example, this query: + *
+ *     query MyQuery {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *             }
+ *             ... @defer {
+ *                 name
+ *             }
+ *         }
+ *     }
+ * 
+ * + * Would result in one ENF (name) associated with 2 `DeferExecution` instances. This is relevant for the execution + * since the field would have to be included in 2 incremental payloads. (I know, there's some duplication here, but + * this is the current state of the spec. There are some discussions happening around de-duplicating data in scenarios + * like this, so this behaviour might change in the future). + * + * A `DeferExecution` may be associated with a list of possible types + * + * For example, this query: + *
+ *     query MyQuery {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *             }
+ *         }
+ *     }
+ * 
+ * results in a `DeferExecution` with no label and possible types [Dog, Cat] + * + * A `DeferExecution` may be associated with specific types + * For example, this query: + *
+ *     query MyQuery {
+ *         animal {
+ *             ... on Cat @defer {
+ *                 name
+ *             }
+ *             ... on Dog {
+ *                 name
+ *             }
+ *         }
+ *     }
+ * 
+ * results in a single ENF (name) associated with a `DeferExecution` with only "Cat" as a possible type. This means + * that, at execution time, `name` should be deferred only if the return object is a "Cat" (but not a if it is a "Dog"). + * + * ENFs associated with the same instance of `DeferExecution` will be resolved in the same incremental response payload + * For example, take these queries: + * + *
+ *     query Query1 {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *             }
+ *             ... @defer {
+ *                 age
+ *             }
+ *         }
+ *     }
+ *
+ *     query Query2 {
+ *         animal {
+ *             ... @defer {
+ *                 name
+ *                 age
+ *             }
+ *         }
+ *     }
+ * 
+ * + * In `Query1`, the ENFs name and age are associated with different instances of `DeferExecution`. This means that, + * during execution, `name` and `age` can be delivered at different times (if name is resolved faster, it will be + * delivered first, and vice-versa). + * In `Query2` the fields will share the same instance of `DeferExecution`. This ensures that, at execution time, the + * fields are guaranteed to be delivered together. In other words, execution should wait until the slowest field resolves + * and deliver both fields at the same time. + * */ @Internal public class DeferExecution { @@ -28,7 +121,7 @@ public String getLabel() { } /** - * TODO Javadoc + * @return the concrete object types that are associated with this defer execution */ public Set getPossibleTypes() { return possibleTypes; From b22fe7a4bfffc7b499c30de100ec814c3fdd6294 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 11 Jan 2024 08:25:28 +1000 Subject: [PATCH 33/33] minor changes --- src/main/java/graphql/Directives.java | 8 +++++++- .../graphql/normalized/incremental/DeferExecution.java | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index fe207f47c8..8e0c81661e 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -81,10 +81,16 @@ public class Directives { /** * The @defer directive can be used to defer sending data for a fragment until later in the query. * This is an opt-in directive that is not available unless it is explicitly put into the schema. + *

+ * This implementation is based on the state of Defer/Stream PR + * More specifically at the state of this + * commit + *

+ * The execution behaviour should match what we get from running Apollo Server 4.9.5 with graphql-js v17.0.0-alpha.2 */ @ExperimentalApi public static final GraphQLDirective DeferDirective = GraphQLDirective.newDirective() - .name("defer") + .name(DEFER) .description("This directive allows results to be deferred during execution") .validLocations(FRAGMENT_SPREAD, INLINE_FRAGMENT) .argument(newArgument() diff --git a/src/main/java/graphql/normalized/incremental/DeferExecution.java b/src/main/java/graphql/normalized/incremental/DeferExecution.java index 0f2593c2b2..71488b03ca 100644 --- a/src/main/java/graphql/normalized/incremental/DeferExecution.java +++ b/src/main/java/graphql/normalized/incremental/DeferExecution.java @@ -1,6 +1,6 @@ package graphql.normalized.incremental; -import graphql.Internal; +import graphql.ExperimentalApi; import graphql.schema.GraphQLObjectType; import javax.annotation.Nullable; @@ -102,7 +102,7 @@ * and deliver both fields at the same time. * */ -@Internal +@ExperimentalApi public class DeferExecution { private final String label; private final Set possibleTypes;