diff --git a/src/main/java/com/intuit/graphql/orchestrator/GraphQLOrchestrator.java b/src/main/java/com/intuit/graphql/orchestrator/GraphQLOrchestrator.java index 72024790..b0b71ab8 100644 --- a/src/main/java/com/intuit/graphql/orchestrator/GraphQLOrchestrator.java +++ b/src/main/java/com/intuit/graphql/orchestrator/GraphQLOrchestrator.java @@ -1,6 +1,7 @@ package com.intuit.graphql.orchestrator; import com.intuit.graphql.orchestrator.deferDirective.DeferDirectiveInstrumentation; +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions; import com.intuit.graphql.orchestrator.schema.RuntimeGraph; import com.intuit.graphql.orchestrator.utils.MultiEIGenerator; import graphql.ExecutionInput; @@ -41,6 +42,8 @@ public class GraphQLOrchestrator { public static final String DATA_LOADER_REGISTRY_CONTEXT_KEY = DataLoaderRegistry.class.getName() + ".context.key"; + private static final DeferOptions DEFAULT_DEFER_OPTIONS = DeferOptions.builder().nestedDefersAllowed(false).build(); + private static final boolean DISABLED_DEFER = false; private final RuntimeGraph runtimeGraph; private final List instrumentations; @@ -80,12 +83,12 @@ private DataLoaderRegistry buildNewDataLoaderRegistry() { } public CompletableFuture execute(ExecutionInput executionInput) { - return execute(executionInput, false); + return execute(executionInput, DEFAULT_DEFER_OPTIONS, DISABLED_DEFER); } - public CompletableFuture execute(ExecutionInput executionInput, boolean hasDefer) { + public CompletableFuture execute(ExecutionInput executionInput, DeferOptions deferOptions, boolean hasDefer) { if(hasDefer) { - return executeWithDefer(executionInput); + return executeWithDefer(executionInput, deferOptions); } final GraphQL graphQL = constructGraphQL(); @@ -101,9 +104,9 @@ public CompletableFuture execute(ExecutionInput executionInput, return graphQL.executeAsync(newExecutionInput); } - private CompletableFuture executeWithDefer(ExecutionInput executionInput) { + private CompletableFuture executeWithDefer(ExecutionInput executionInput, DeferOptions options) { AtomicInteger responses = new AtomicInteger(0); - MultiEIGenerator eiGenerator = new MultiEIGenerator(executionInput); + MultiEIGenerator eiGenerator = new MultiEIGenerator(executionInput, options, this.getSchema()); Flux executionResultPublisher = eiGenerator.generateEIs() .filter(ei -> !ei.getQuery().equals("")) diff --git a/src/main/java/com/intuit/graphql/orchestrator/authorization/DownstreamQueryRedactor.java b/src/main/java/com/intuit/graphql/orchestrator/authorization/DownstreamQueryRedactor.java index 859a0238..e0de8ddd 100644 --- a/src/main/java/com/intuit/graphql/orchestrator/authorization/DownstreamQueryRedactor.java +++ b/src/main/java/com/intuit/graphql/orchestrator/authorization/DownstreamQueryRedactor.java @@ -3,7 +3,6 @@ import com.intuit.graphql.orchestrator.batch.AuthDownstreamQueryModifier; import com.intuit.graphql.orchestrator.schema.ServiceMetadata; import com.intuit.graphql.orchestrator.utils.SelectionCollector; -import graphql.language.AstTransformer; import graphql.language.FragmentDefinition; import graphql.language.Node; import graphql.schema.DataFetchingEnvironment; @@ -13,11 +12,10 @@ import java.util.Map; +import static com.intuit.graphql.orchestrator.utils.GraphQLUtil.AST_TRANSFORMER; + @Builder public class DownstreamQueryRedactor { - - private static final AstTransformer AST_TRANSFORMER = new AstTransformer(); - @NonNull private Node root; @NonNull private GraphQLType rootType; @NonNull private GraphQLType rootParentType; diff --git a/src/main/java/com/intuit/graphql/orchestrator/deferDirective/DeferOptions.java b/src/main/java/com/intuit/graphql/orchestrator/deferDirective/DeferOptions.java new file mode 100644 index 00000000..16a4f853 --- /dev/null +++ b/src/main/java/com/intuit/graphql/orchestrator/deferDirective/DeferOptions.java @@ -0,0 +1,10 @@ +package com.intuit.graphql.orchestrator.deferDirective; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class DeferOptions { + private boolean nestedDefersAllowed; +} diff --git a/src/main/java/com/intuit/graphql/orchestrator/deferDirective/DeferUtil.java b/src/main/java/com/intuit/graphql/orchestrator/deferDirective/DeferUtil.java new file mode 100644 index 00000000..f5fc1f29 --- /dev/null +++ b/src/main/java/com/intuit/graphql/orchestrator/deferDirective/DeferUtil.java @@ -0,0 +1,53 @@ +package com.intuit.graphql.orchestrator.deferDirective; + +import graphql.language.Argument; +import graphql.language.BooleanValue; +import graphql.language.Directive; +import graphql.language.DirectivesContainer; +import graphql.language.Node; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import lombok.NonNull; + +import java.util.List; + +import static com.intuit.graphql.orchestrator.utils.DirectivesUtil.DEFER_DIRECTIVE_NAME; +import static com.intuit.graphql.orchestrator.utils.DirectivesUtil.DEFER_IF_ARG; + +public class DeferUtil { + /** + * Checks if it is necessary to create ei for deferred field. + * Currently, selection should be skipped if all the children field are deferred resulting in an empty selection set. + * @param selection: node to check if children are all deferred + * @return boolean: true if all children are deferred, false otherwise + */ + public static boolean hasNonDeferredSelection(@NonNull Selection selection) { + return ((List)selection.getChildren()) + .stream() + .filter(SelectionSet.class::isInstance) + .map(SelectionSet.class::cast) + .findAny() + .get() + .getSelections() + .stream() + .anyMatch(child -> !containsEnabledDeferDirective(child)); + } + + /** + * Verifies that Node has defer directive that is not disabled + * @param node: node to check if contains defer directive + * @return boolean: true if node has an enabled defer, false otherwise + */ + public static boolean containsEnabledDeferDirective(Selection node) { + return node instanceof DirectivesContainer && + ((List) ((DirectivesContainer) node).getDirectives()) + .stream() + .filter(directive -> DEFER_DIRECTIVE_NAME.equals(directive.getName())) + .findFirst() + .map(directive -> { + Argument ifArg = directive.getArgument(DEFER_IF_ARG); + return ifArg == null || ((BooleanValue) ifArg.getValue()).isValue(); + }) + .orElse(false); + } +} diff --git a/src/main/java/com/intuit/graphql/orchestrator/utils/MultiEIGenerator.java b/src/main/java/com/intuit/graphql/orchestrator/utils/MultiEIGenerator.java index a2eced83..ca9ba947 100644 --- a/src/main/java/com/intuit/graphql/orchestrator/utils/MultiEIGenerator.java +++ b/src/main/java/com/intuit/graphql/orchestrator/utils/MultiEIGenerator.java @@ -1,25 +1,41 @@ package com.intuit.graphql.orchestrator.utils; import com.google.common.annotations.VisibleForTesting; +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions; +import com.intuit.graphql.orchestrator.visitors.queryVisitors.DeferQueryExtractor; import graphql.ExecutionInput; +import graphql.analysis.QueryTransformer; +import graphql.language.Document; +import graphql.language.FragmentDefinition; +import graphql.language.OperationDefinition; +import graphql.language.SelectionSet; +import graphql.schema.GraphQLSchema; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.intuit.graphql.orchestrator.utils.GraphQLUtil.parser; @Slf4j public class MultiEIGenerator { - - private List eis = new ArrayList<>(); + private final List eis = new ArrayList<>(); + private final DeferOptions deferOptions; + private final GraphQLSchema schema; private Integer numOfEIs = null; - private final static String EMPTY_QUERY =""; @VisibleForTesting private long timeProcessedSplit = 0; - public MultiEIGenerator(ExecutionInput ei) { + public MultiEIGenerator(ExecutionInput ei, DeferOptions deferOptions, GraphQLSchema schema) { this.eis.add(ei); + this.deferOptions = deferOptions; + this.schema = schema; } public Flux generateEIs() { @@ -39,7 +55,40 @@ public Flux generateEIs() { this.timeProcessedSplit = System.currentTimeMillis(); //Adds elements to list of eis that need to be processed try { - this.eis.addAll(MultipartUtil.splitMultipartExecutionInput(emittedEI)); + Document rootDocument = parser.parseDocument(emittedEI.getQuery()); + Map fragmentDefinitionMap = rootDocument.getDefinitionsOfType(FragmentDefinition.class) + .stream() + .collect(Collectors.toMap(FragmentDefinition::getName , Function.identity())); + + ExecutionInput finalEmittedEI = emittedEI; + AtomicReference operationDefinitionReference = new AtomicReference<>(); + rootDocument.getDefinitionsOfType(OperationDefinition.class) + .stream() + .peek(operationDefinitionReference::set) + .map(OperationDefinition::getSelectionSet) + .map(SelectionSet::getSelections) + .flatMap(List::stream) + .forEach(selection -> { + QueryTransformer transformer = QueryTransformer.newQueryTransformer() + .schema(this.schema) + .root(selection) + .rootParentType(this.schema.getQueryType()) + .fragmentsByName(fragmentDefinitionMap) + .variables(finalEmittedEI.getVariables()) + .build(); + + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .deferOptions(deferOptions) + .originalEI(finalEmittedEI) + .rootNode(rootDocument) + .operationDefinition(operationDefinitionReference.get()) + .fragmentDefinitionMap(fragmentDefinitionMap) + .build(); + + transformer.transform(visitor); + + this.eis.addAll(visitor.getExtractedEIs()); + }); } catch (Exception ex) { sink.error(ex); diff --git a/src/main/java/com/intuit/graphql/orchestrator/utils/NodeUtils.java b/src/main/java/com/intuit/graphql/orchestrator/utils/NodeUtils.java new file mode 100644 index 00000000..f06c8d74 --- /dev/null +++ b/src/main/java/com/intuit/graphql/orchestrator/utils/NodeUtils.java @@ -0,0 +1,98 @@ +package com.intuit.graphql.orchestrator.utils; + +import graphql.language.Directive; +import graphql.language.DirectivesContainer; +import graphql.language.Field; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.InlineFragment; +import graphql.language.Node; +import graphql.language.OperationDefinition; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import org.apache.commons.lang3.ObjectUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class NodeUtils { + + /** + * Transforms node by removing desired directive from the node. + * Returns transformed node + * Throws an exception if it is at a location that is not supported. + * @param node: the selection that you would like to prune + * @param directiveName: the directive you would like to search for + * @return selection: pruned selection + * */ + public static Selection removeDirectiveFromNode(Selection node, String directiveName) { + //DirectivesContainer returns getDirectives as a List so need to cast to stream correctly + List prunedDirectives = ((List)((DirectivesContainer)node).getDirectives()) + .stream() + .filter(directive -> ObjectUtils.notEqual(directiveName, directive.getName())) + .collect(Collectors.toList()); + + if(node instanceof Field) { + return ((Field) node).transform(builder -> builder.directives(prunedDirectives)); + } else if(node instanceof InlineFragment) { + return ((InlineFragment) node).transform(builder -> builder.directives(prunedDirectives)); + } else { + return ((FragmentSpread) node).transform(builder -> builder.directives(prunedDirectives)); + } + } + + /** + * Check if the selection has children selections + * @param node selection that you are trying to check + * @return boolean: true if selection doesn't have selectionset as child, false otherwise + * */ + public static boolean isLeaf(Selection node) { + return node.getChildren().isEmpty() || + node.getChildren() + .stream() + .noneMatch(SelectionSet.class::isInstance); + } + + + /** + * Generates new node + * Transforms the parentNode with a new selection set consisting of the pruned child and typename fields + * @param parentNode node that will be transformed and will contain only selections + * @param selections selections that need to be in selection set for parentNode + * @return node that only contains the passed in selections + * */ + public static Node transformNodeWithSelections(Node parentNode, Selection... selections) { + SelectionSet prunedSelectionSet = SelectionSet.newSelectionSet() + .selections(Arrays.asList(selections)) + .build(); + + if(parentNode instanceof Field) { + return ((Field) parentNode).transform(builder -> builder.selectionSet(prunedSelectionSet)); + } else if (parentNode instanceof FragmentDefinition) { + //add fragment spread names here in case of nested fragment spreads + return ((FragmentDefinition) parentNode).transform(builder -> builder.selectionSet(prunedSelectionSet)); + } else if (parentNode instanceof InlineFragment) { + return ((InlineFragment) parentNode).transform(builder -> builder.selectionSet(prunedSelectionSet)); + } else { + return ((OperationDefinition) parentNode).transform(builder -> builder.selectionSet(prunedSelectionSet)); + } + } + + /** + * Retrieves the objects from map matching key + * @param kvMap: map that will be searched + * @param keyCollection keys to check + * @return list of values from map with matching key + * */ + public static List getAllMapValuesWithMatchingKeys(Map kvMap, Collection keyCollection) { + return keyCollection + .stream() + .map(kvMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/intuit/graphql/orchestrator/utils/QueryPathUtils.java b/src/main/java/com/intuit/graphql/orchestrator/utils/QueryPathUtils.java index ee81538a..89f1f0e3 100644 --- a/src/main/java/com/intuit/graphql/orchestrator/utils/QueryPathUtils.java +++ b/src/main/java/com/intuit/graphql/orchestrator/utils/QueryPathUtils.java @@ -4,11 +4,12 @@ import graphql.language.FragmentDefinition; import graphql.language.Node; import graphql.util.TraverserContext; +import org.apache.commons.lang3.StringUtils; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; public class QueryPathUtils { diff --git a/src/main/java/com/intuit/graphql/orchestrator/utils/TraverserContextUtils.java b/src/main/java/com/intuit/graphql/orchestrator/utils/TraverserContextUtils.java new file mode 100644 index 00000000..715bb198 --- /dev/null +++ b/src/main/java/com/intuit/graphql/orchestrator/utils/TraverserContextUtils.java @@ -0,0 +1,22 @@ +package com.intuit.graphql.orchestrator.utils; + +import graphql.language.Node; +import graphql.language.SelectionSetContainer; +import graphql.util.TraverserContext; + +import java.util.List; +import java.util.stream.Collectors; + +public class TraverserContextUtils { + /** + * Returns the current nodes parent node definitions + * @param currentNode context for current node + * @return List of graphql definitions + * */ + public static List getParentDefinitions(TraverserContext currentNode) { + return currentNode.getParentNodes() + .stream() + .filter(SelectionSetContainer.class::isInstance) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/intuit/graphql/orchestrator/visitors/queryVisitors/DeferQueryExtractor.java b/src/main/java/com/intuit/graphql/orchestrator/visitors/queryVisitors/DeferQueryExtractor.java new file mode 100644 index 00000000..6bb693c8 --- /dev/null +++ b/src/main/java/com/intuit/graphql/orchestrator/visitors/queryVisitors/DeferQueryExtractor.java @@ -0,0 +1,170 @@ +package com.intuit.graphql.orchestrator.visitors.queryVisitors; + +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions; +import graphql.ExecutionInput; +import graphql.analysis.QueryVisitorFieldEnvironment; +import graphql.analysis.QueryVisitorFragmentSpreadEnvironment; +import graphql.analysis.QueryVisitorInlineFragmentEnvironment; +import graphql.analysis.QueryVisitorStub; +import graphql.language.AstPrinter; +import graphql.language.Definition; +import graphql.language.Document; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.Node; +import graphql.language.OperationDefinition; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import graphql.util.TraverserContext; +import graphql.util.TreeTransformerUtil; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.intuit.graphql.orchestrator.deferDirective.DeferUtil.containsEnabledDeferDirective; +import static com.intuit.graphql.orchestrator.deferDirective.DeferUtil.hasNonDeferredSelection; +import static com.intuit.graphql.orchestrator.utils.DirectivesUtil.DEFER_DIRECTIVE_NAME; +import static com.intuit.graphql.orchestrator.utils.GraphQLUtil.AST_TRANSFORMER; +import static com.intuit.graphql.orchestrator.utils.IntrospectionUtil.__typenameField; +import static com.intuit.graphql.orchestrator.utils.NodeUtils.getAllMapValuesWithMatchingKeys; +import static com.intuit.graphql.orchestrator.utils.NodeUtils.isLeaf; +import static com.intuit.graphql.orchestrator.utils.NodeUtils.removeDirectiveFromNode; +import static com.intuit.graphql.orchestrator.utils.NodeUtils.transformNodeWithSelections; +import static com.intuit.graphql.orchestrator.utils.TraverserContextUtils.getParentDefinitions; + +@Builder +public class DeferQueryExtractor extends QueryVisitorStub { + + @NonNull private final Document rootNode; + @NonNull private final OperationDefinition operationDefinition; + @Builder.Default + @NonNull + private Map fragmentDefinitionMap = new HashMap<>(); + @NonNull private final PruneChildDeferSelectionsModifier childModifier; + @NonNull private ExecutionInput originalEI; + + @NonNull private DeferOptions deferOptions; + + @Getter + private final List extractedEIs = new ArrayList<>(); + + /** + * Functions: + * Updates the field if it contains defer directive. + * If it is a valid deferred node, generate new EI and add to list to be extracted + * @param queryVisitorFieldEnvironment field that is being visited + */ + @Override + public void visitField(QueryVisitorFieldEnvironment queryVisitorFieldEnvironment) { + updateDeferredInfoForNode(queryVisitorFieldEnvironment.getField(), queryVisitorFieldEnvironment.getTraverserContext()); + } + + /** + * Functions: + * Updates the inline fragment if it contains defer directive. + * If it is a valid deferred node, generate new EI and add to list to be extracted + * @param queryVisitorInlineFragmentEnvironment field that is being visited + */ + @Override + public void visitInlineFragment(QueryVisitorInlineFragmentEnvironment queryVisitorInlineFragmentEnvironment) { + updateDeferredInfoForNode(queryVisitorInlineFragmentEnvironment.getInlineFragment(), queryVisitorInlineFragmentEnvironment.getTraverserContext()); + } + + /** + * Functions: + * Updates the fragment spread if it contains defer directive. + * If it is a valid deferred node, generate new EI and add to list to be extracted + * @param queryVisitorFragmentSpreadEnvironment field that is being visited + */ + @Override + public void visitFragmentSpread(QueryVisitorFragmentSpreadEnvironment queryVisitorFragmentSpreadEnvironment) { + updateDeferredInfoForNode(queryVisitorFragmentSpreadEnvironment.getFragmentSpread(), queryVisitorFragmentSpreadEnvironment.getTraverserContext()); + } + + /** + * Updates node if it contains defer + * @param node node that is currently being visited + * @param context context for traversed node + * */ + private void updateDeferredInfoForNode(Selection node, TraverserContext context) { + if(containsEnabledDeferDirective(node)) { + Selection prunedNode = removeDirectiveFromNode(node, DEFER_DIRECTIVE_NAME); + + if(isLeaf(node) || hasNonDeferredSelection(node)) { + extractedEIs.add(generateDeferredEI(prunedNode, context)); + } + + //update node so children nodes has the correct definition + TreeTransformerUtil.changeNode(context, prunedNode); + } + } + + /** + * Generates an Execution Input given the node and the context + * @param context current selection + * @param currentNode context for the selection + * @return An ExecutionInput + * */ + private ExecutionInput generateDeferredEI(Selection currentNode, TraverserContext context) { + //prune defer information from children + Node prunedNode = AST_TRANSFORMER.transform(currentNode, childModifier); + List parentNodes = getParentDefinitions(context); + + Set neededFragmentSpreads = new HashSet<>(); + if(currentNode instanceof FragmentSpread) { + neededFragmentSpreads.add(((FragmentSpread) currentNode).getName()); + } + + //builds parent nodes with pruned information + for (Node parentNode : parentNodes) { + prunedNode = transformNodeWithSelections(parentNode, (Selection)prunedNode, __typenameField); + + if(parentNode instanceof FragmentSpread) { + neededFragmentSpreads.add(((FragmentSpread) parentNode).getName()); + } + } + + //Gets all the definitions for the fragment spreads + List fragmentSpreadDefs = getAllMapValuesWithMatchingKeys(fragmentDefinitionMap, neededFragmentSpreads); + + SelectionSet ss = SelectionSet.newSelectionSet().selection((Selection) prunedNode).build(); + //builds new OperationDefinition consisting with only pruned nodes + OperationDefinition newOperation = this.operationDefinition.transform(builder -> builder.selectionSet(ss)); + + List deferredDefinitions = new ArrayList<>(); + deferredDefinitions.add(newOperation); + deferredDefinitions.addAll(fragmentSpreadDefs); + + Document deferredDocument = this.rootNode.transform(builder -> builder.definitions(deferredDefinitions)); + + String query = AstPrinter.printAst(deferredDocument); + + return originalEI.transform(builder -> builder.query(query)); + } + + /** + * Builder for class + * */ + public static class DeferQueryExtractorBuilder { + PruneChildDeferSelectionsModifier childModifier; + DeferOptions deferOptions; + + public DeferQueryExtractorBuilder deferOptions(DeferOptions deferOptions) { + this.deferOptions = deferOptions; + return childModifier(PruneChildDeferSelectionsModifier.builder().deferOptions(deferOptions).build()); + } + + private DeferQueryExtractorBuilder childModifier(PruneChildDeferSelectionsModifier childModifier) { + this.childModifier = childModifier; + return this; + } + + } +} diff --git a/src/main/java/com/intuit/graphql/orchestrator/visitors/queryVisitors/PruneChildDeferSelectionsModifier.java b/src/main/java/com/intuit/graphql/orchestrator/visitors/queryVisitors/PruneChildDeferSelectionsModifier.java new file mode 100644 index 00000000..0cd171a5 --- /dev/null +++ b/src/main/java/com/intuit/graphql/orchestrator/visitors/queryVisitors/PruneChildDeferSelectionsModifier.java @@ -0,0 +1,122 @@ +package com.intuit.graphql.orchestrator.visitors.queryVisitors; + +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions; +import graphql.GraphQLException; +import graphql.language.Directive; +import graphql.language.DirectivesContainer; +import graphql.language.Field; +import graphql.language.FragmentSpread; +import graphql.language.InlineFragment; +import graphql.language.Node; +import graphql.language.NodeVisitorStub; +import graphql.language.Selection; +import graphql.language.SelectionSet; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +import lombok.Builder; + +import static com.intuit.graphql.orchestrator.deferDirective.DeferUtil.containsEnabledDeferDirective; +import static com.intuit.graphql.orchestrator.deferDirective.DeferUtil.hasNonDeferredSelection; +import static com.intuit.graphql.orchestrator.utils.DirectivesUtil.DEFER_DIRECTIVE_NAME; +import static com.intuit.graphql.orchestrator.utils.IntrospectionUtil.__typenameField; +import static com.intuit.graphql.orchestrator.utils.NodeUtils.isLeaf; +import static com.intuit.graphql.orchestrator.utils.NodeUtils.removeDirectiveFromNode; +import static graphql.util.TreeTransformerUtil.changeNode; +import static graphql.util.TreeTransformerUtil.deleteNode; + +@Builder +public class PruneChildDeferSelectionsModifier extends NodeVisitorStub { + private DeferOptions deferOptions; + + /** + * Visits Field and deletes defer information or throws an exception + * @param node current node traverser is visiting + * @param context context for the current node + * @return TraversalControl whether to continue or abort + * */ + @Override + public TraversalControl visitField(Field node, TraverserContext context) { + return deleteDeferIfExists(node, context); + } + + /** + * Visits FragmentSpread and deletes defer information or throws an exception + * @param node current node traverser is visiting + * @param context context for the current node + * @return TraversalControl whether to continue or abort + * */ + @Override + public TraversalControl visitFragmentSpread(FragmentSpread node, TraverserContext context) { + return deleteDeferIfExists(node, context); + } + + /** + * Visits Inline Fragment and deletes defer information or throws an exception + * @param node current node traverser is visiting + * @param context context for the current node + * @return TraversalControl whether to continue or abort + * */ + @Override + public TraversalControl visitInlineFragment(InlineFragment node, TraverserContext context) { + return deleteDeferIfExists(node, context); + } + + /** + * Visits SelectionSet and add typename to selection set + * @param node current node traverser is visiting + * @param context context for the current node + * @return TraversalControl whether to continue or abort + * */ + @Override + public TraversalControl visitSelectionSet(SelectionSet node, TraverserContext context) { + SelectionSet newSelectionSet = node.transform(builder -> builder.selection(__typenameField)); + + return changeNode(context, newSelectionSet); + } + + /** + * Visits Directive and deletes defer information + * @param node current node traverser is visiting + * @param context context for the current node + * @return TraversalControl whether to continue or abort + * */ + public TraversalControl visitDirective(Directive node, TraverserContext context) { + //removes unnecessary and disabled defer directive if exists + if(DEFER_DIRECTIVE_NAME.equals(node.getName())) { + deleteNode(context); + } + + return this.visitNode(node, context); + } + + /** + * deletes defer information or throws an exception for selection + * @param node current node traverser is visiting + * @param context context for the current node + * @return TraversalControl whether to continue or abort + * */ + private TraversalControl deleteDeferIfExists(Selection node, TraverserContext context) { + //skip if it does not have defer directive + if(((DirectivesContainer)node).hasDirective(DEFER_DIRECTIVE_NAME)) { + if(containsEnabledDeferDirective(node)) { + //if node has an enabled defer, check if option allows it + if(!this.deferOptions.isNestedDefersAllowed()) { + throw new GraphQLException("Nested defers are currently unavailable."); + } + + if(isLeaf(node) || hasNonDeferredSelection(node)) { + //delete node if it is enabled because extractor will create query for it + return deleteNode(context); + } else { + //remove directive so it is not included in downstream query + return changeNode(context, removeDirectiveFromNode(node, DEFER_DIRECTIVE_NAME)); + } + } else { + //remove directive so it is not included in downstream query + return changeNode(context, removeDirectiveFromNode(node, DEFER_DIRECTIVE_NAME)); + } + } + + return TraversalControl.CONTINUE; + } +} diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/integration/QueryDirectiveSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/integration/QueryDirectiveSpec.groovy index 93f7450d..9c1840d8 100644 --- a/src/test/groovy/com/intuit/graphql/orchestrator/integration/QueryDirectiveSpec.groovy +++ b/src/test/groovy/com/intuit/graphql/orchestrator/integration/QueryDirectiveSpec.groovy @@ -3,6 +3,7 @@ package com.intuit.graphql.orchestrator.integration import com.google.common.collect.ImmutableMap import com.intuit.graphql.orchestrator.GraphQLOrchestrator import com.intuit.graphql.orchestrator.ServiceProvider +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions import com.intuit.graphql.orchestrator.testhelpers.MockServiceProvider import com.intuit.graphql.orchestrator.testhelpers.SimpleMockServiceProvider import com.intuit.graphql.orchestrator.utils.GraphQLUtil @@ -308,7 +309,11 @@ class QueryDirectiveSpec extends BaseIntegrationTestSpecification { } ''').build() - ExecutionResult executionResult = specUnderTest.execute(petsEI, true).get() + DeferOptions deferOptions = DeferOptions.builder() + .nestedDefersAllowed(true) + .build() + + ExecutionResult executionResult = specUnderTest.execute(petsEI, deferOptions,true).get() SubscriptionPublisher subscriptionPublisher = (SubscriptionPublisher)executionResult.data Flux publisher = (Flux) subscriptionPublisher.upstreamPublisher; List results = (List) publisher.collectList().block() diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/utils/DeferUtilSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/utils/DeferUtilSpec.groovy new file mode 100644 index 00000000..b9a8cb26 --- /dev/null +++ b/src/test/groovy/com/intuit/graphql/orchestrator/utils/DeferUtilSpec.groovy @@ -0,0 +1,167 @@ +package com.intuit.graphql.orchestrator.utils + +import graphql.language.* +import helpers.BaseIntegrationTestSpecification + +import static com.intuit.graphql.orchestrator.deferDirective.DeferUtil.containsEnabledDeferDirective +import static com.intuit.graphql.orchestrator.deferDirective.DeferUtil.hasNonDeferredSelection + +class DeferUtilSpec extends BaseIntegrationTestSpecification{ + Directive enabledDefer = Directive.newDirective().name("defer").build() + Directive disabledDefer = Directive.newDirective() + .name("defer") + .argument( + Argument.newArgument("if", BooleanValue.of(false)) + .build()) + .build() + + def "hasNonDeferredSelection throws exception if node is null"(){ + when: + hasNonDeferredSelection(null) + + then: + thrown(NullPointerException) + } + + def "hasNonDeferredSelection return true if node has child selections do not have defer"(){ + when: + Field childField1 = Field.newField("childField").build() + Field childField2 = Field.newField("childField").build() + SelectionSet ss = SelectionSet.newSelectionSet() + .selection(childField1) + .selection(childField2) + .build() + Field parentField = Field.newField("parentNode", ss).build() + + then: + hasNonDeferredSelection(parentField) + } + + def "hasNonDeferredSelection return true if node has child selections with one defer and others arent"(){ + when: + Field childField1 = Field.newField("childField").directive(enabledDefer).build() + Field childField2 = Field.newField("childField").build() + SelectionSet ss = SelectionSet.newSelectionSet() + .selection(childField1) + .selection(childField2) + .build() + Field parentField = Field.newField("parentNode", ss).build() + + then: + hasNonDeferredSelection(parentField) + } + + def "hasNonDeferredSelection return true if node has child selections that are disabled"(){ + when: + Field childField1 = Field.newField("childField").directive(disabledDefer).build() + Field childField2 = Field.newField("childField").build() + SelectionSet ss = SelectionSet.newSelectionSet() + .selection(childField1) + .selection(childField2) + .build() + Field parentField = Field.newField("parentNode", ss).build() + + then: + hasNonDeferredSelection(parentField) + } + + def "hasNonDeferredSelection return false if node has child selections and all are deferred"(){ + when: + Field childField1 = Field.newField("childField1").directive(enabledDefer).build() + Field childField2 = Field.newField("childField2").directive(enabledDefer).build() + SelectionSet ss = SelectionSet.newSelectionSet() + .selection(childField1) + .selection(childField2) + .build() + Field parentField = Field.newField("parentNode", ss).build() + + then: + !hasNonDeferredSelection(parentField) + } + + def "containsEnabledDeferDirective returns false for non DirectiveContainer"() { + when: + SelectionCollectorSpec.NewImplementationSelection node = new SelectionCollectorSpec.NewImplementationSelection() + + then: + !containsEnabledDeferDirective(node) + } + + def "FragmentSpread with defer returns true"() { + when: + FragmentSpread node = FragmentSpread.newFragmentSpread("testFragment").directive(enabledDefer).build() + + then: + containsEnabledDeferDirective(node) + } + + def "FragmentSpread with disabled defer returns false"() { + when: + FragmentSpread node = FragmentSpread.newFragmentSpread("testFragment").directive(disabledDefer).build() + + then: + !containsEnabledDeferDirective(node) + } + + def "FragmentSpread without defer directive returns false"() { + when: + FragmentSpread node = FragmentSpread.newFragmentSpread("testFragment") + .directive(Directive.newDirective().name("testDir").build()) + .build() + + then: + !containsEnabledDeferDirective(node) + } + + def "InlineFragment with defer returns true"() { + when: + InlineFragment node = InlineFragment.newInlineFragment().directive(enabledDefer).build() + + then: + containsEnabledDeferDirective((DirectivesContainer)node) + } + + def "InlineFragment with disabled defer returns false"() { + when: + InlineFragment node = InlineFragment.newInlineFragment().directive(disabledDefer).build() + + then: + !containsEnabledDeferDirective((DirectivesContainer)node) + } + + def "InlineFragment without defer directive returns false"() { + when: + InlineFragment node = InlineFragment.newInlineFragment() + .directive(Directive.newDirective().name("testDir").build()) + .build() + + then: + !containsEnabledDeferDirective((DirectivesContainer)node) + } + + def "Field with defer returns true"() { + when: + Field node = Field.newField("testField").directive(enabledDefer).build() + + then: + containsEnabledDeferDirective((Selection)node) + } + + def "Field with disabled defer returns false"() { + when: + Field node = Field.newField("testField").directive(disabledDefer).build() + + then: + !containsEnabledDeferDirective((Selection)node) + } + + def "Field without defer directive returns false"() { + when: + Field node = Field.newField("testField") + .directive(Directive.newDirective().name("testDir").build()) + .build() + + then: + !containsEnabledDeferDirective(node) + } +} diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/utils/MultiEIGeneratorSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/utils/MultiEIGeneratorSpec.groovy index c0ddf0f5..ce99f359 100644 --- a/src/test/groovy/com/intuit/graphql/orchestrator/utils/MultiEIGeneratorSpec.groovy +++ b/src/test/groovy/com/intuit/graphql/orchestrator/utils/MultiEIGeneratorSpec.groovy @@ -1,13 +1,63 @@ package com.intuit.graphql.orchestrator.utils +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions import graphql.ExecutionInput import graphql.parser.InvalidSyntaxException +import graphql.scalar.GraphqlStringCoercing +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema import reactor.test.StepVerifier import spock.lang.Specification class MultiEIGeneratorSpec extends Specification { MultiEIGenerator multiEIGenerator + DeferOptions options = DeferOptions.builder() + .nestedDefersAllowed(true) + .build() + + GraphQLScalarType scalarType = GraphQLScalarType.newScalar() + .name("scale") + .coercing(new GraphqlStringCoercing()) + .build() + + GraphQLFieldDefinition idField = GraphQLFieldDefinition.newFieldDefinition() + .name("id") + .type(scalarType) + .build() + + GraphQLFieldDefinition nameField = GraphQLFieldDefinition.newFieldDefinition() + .name("name") + .type(scalarType) + .build() + + GraphQLFieldDefinition typeField = GraphQLFieldDefinition.newFieldDefinition() + .name("type") + .type(scalarType) + .build() + + GraphQLObjectType petType = GraphQLObjectType.newObject() + .name("Pet") + .field(idField) + .field(nameField) + .field(typeField) + .build() + + GraphQLFieldDefinition petsQuery = GraphQLFieldDefinition.newFieldDefinition() + .name("pets") + .type(petType) + .build() + + GraphQLObjectType queryType = GraphQLObjectType.newObject() + .name("query") + .field(petsQuery) + .build() + + GraphQLSchema schema = GraphQLSchema.newSchema().query(queryType).additionalType(petType).build() + + def "Generator split query correctly"() { given: @@ -31,7 +81,7 @@ class MultiEIGeneratorSpec extends Specification { ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() when: - multiEIGenerator = new MultiEIGenerator(ei) + multiEIGenerator = new MultiEIGenerator(ei, options, schema) then: StepVerifier.create(multiEIGenerator.generateEIs()) @@ -64,7 +114,7 @@ class MultiEIGeneratorSpec extends Specification { long timeEmitted = 0; when: - multiEIGenerator = new MultiEIGenerator(ei) + multiEIGenerator = new MultiEIGenerator(ei, options, schema) then: StepVerifier.create(multiEIGenerator.generateEIs()) @@ -94,7 +144,7 @@ class MultiEIGeneratorSpec extends Specification { ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() when: - multiEIGenerator = new MultiEIGenerator(ei) + multiEIGenerator = new MultiEIGenerator(ei, options, schema) then: StepVerifier.create(multiEIGenerator.generateEIs()) @@ -109,7 +159,7 @@ class MultiEIGeneratorSpec extends Specification { ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() when: - multiEIGenerator = new MultiEIGenerator(ei) + multiEIGenerator = new MultiEIGenerator(ei, options, schema) then: StepVerifier.create(multiEIGenerator.generateEIs()) diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/utils/MultipartUtilSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/utils/MultipartUtilSpec.groovy deleted file mode 100644 index e344029c..00000000 --- a/src/test/groovy/com/intuit/graphql/orchestrator/utils/MultipartUtilSpec.groovy +++ /dev/null @@ -1,298 +0,0 @@ -package com.intuit.graphql.orchestrator.utils - - -import graphql.ExecutionInput -import lombok.extern.slf4j.Slf4j -import spock.lang.Specification -/** - * Covers test for ObjectTypeExtension, InterfaceTypeExtension, UnionTypeExtension, EnumTypeExtension, - * InputObjectTypeExtension TODO ScalarTypeExtension. - */ -@Slf4j -class MultipartUtilSpec extends Specification { - - def "can split Execution input"() { - given: - String query = "query { queryA { fieldA fieldB fieldC @defer } }" - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 1 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " fieldC\n" + - " __typename\n" + - " }\n" + - "}\n" - } - - def "can split EI with alias selections"(){ - given: - String query = "query { queryA { aliasA: fieldA aliasB: fieldB @defer } }" - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 1 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " aliasB: fieldB\n" + - " __typename\n" + - " }\n" + - "}\n" - } - - def "can split execution input with arguments"() { - given: - String query = "query { getFoo(id: \"inputA\") { fieldA fieldB @defer } }" - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 1 - splitSet.get(0).query == "query {\n" + - " getFoo(id: \"inputA\") {\n" + - " fieldB\n" + - " __typename\n" + - " }\n" + - "}\n" - } - - def "can split EI with multiple defer on same level"() { - given: - String query = "query { queryA { fieldA fieldB @defer fieldC @defer } }" - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 2 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " fieldB\n" + - " __typename\n" + - " }\n" + - "}\n" - splitSet.get(1).query == "query {\n" + - " queryA {\n" + - " fieldC\n" + - " __typename\n" + - " }\n" + - "}\n" - - - } - - def "can split EI with nested deferred selections"() { - given: - String query = "query { queryA { fieldA objectField { fieldB fieldC @defer } } }" - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 1 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " objectField {\n" + - " fieldC\n" + - " __typename\n" + - " }\n" + - " __typename\n" + - " }\n" + - "}\n" - } - - def "Does not split EI when if arg is false"() { - given: - String query = "query { queryA { fieldA fieldB fieldC @defer(if: false) } }" - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 0 - } - - //todo - def "prunes selections sets without fields after removing deferred fields"() {} - - //todo - def "can split EI with variables" () {} - - def "can split EI with inline fragment"() { - given: - String query = """ - query { - queryA { - fieldA - objectField { - fieldB - } - ... on ObjectType @defer { - fieldC - } - } - } - """ - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 1 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " ... on ObjectType {\n" + - " fieldC\n" + - " }\n" + - " __typename\n" + - " }\n" + - "}\n" - } - - def "split EI has correct inline fragments different types"() { - given: - String query = """ - query { - queryA { - fieldA - objectField1 { - fieldB - } - ... on ObjectType1 @defer { - fieldC - } - objectField2 { - fieldD - } - ... on ObjectType2 @defer { - fieldE - } - } - } - """ - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 2 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " ... on ObjectType1 {\n" + - " fieldC\n" + - " }\n" + - " __typename\n" + - " }\n" + - "}\n" - splitSet.get(1).query == "query {\n" + - " queryA {\n" + - " ... on ObjectType2 {\n" + - " fieldE\n" + - " }\n" + - " __typename\n" + - " }\n" + - "}\n" - } - - def "split EI has correct inline fragments non merged types"() { - given: - String query = """ - query { - queryA { - fieldA - objectField1 { - fieldB - } - ... on ObjectType @defer { - fieldC - } - objectField2 { - fieldD - } - ... on ObjectType @defer { - fieldE - } - } - } - """ - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 2 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " ... on ObjectType {\n" + - " fieldC\n" + - " }\n" + - " __typename\n" + - " }\n" + - "}\n" - splitSet.get(1).query == "query {\n" + - " queryA {\n" + - " ... on ObjectType {\n" + - " fieldE\n" + - " }\n" + - " __typename\n" + - " }\n" + - "}\n" - } - - def "can split EI with fragment spread"() { - given: - String query = """ - query { - queryA { - fieldA - objectField { - fieldB - ... deferredInfo @defer - } - - } - } - fragment deferredInfo on ObjectType { - fieldC - } - """ - - when: - ExecutionInput input = ExecutionInput.newExecutionInput(query).build() - List splitSet = MultipartUtil.splitMultipartExecutionInput(input) - - then: - splitSet.size() == 1 - splitSet.get(0).query == "query {\n" + - " queryA {\n" + - " objectField {\n" + - " ...deferredInfo\n" + - " __typename\n" + - " }\n" + - " __typename\n" + - " }\n" + - "}\n" + - "\nfragment deferredInfo on ObjectType {\n" + - " fieldC\n" + - "}\n" - } - - // end to end tests - - -} diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/utils/NodeUtilsSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/utils/NodeUtilsSpec.groovy new file mode 100644 index 00000000..bcad31a4 --- /dev/null +++ b/src/test/groovy/com/intuit/graphql/orchestrator/utils/NodeUtilsSpec.groovy @@ -0,0 +1,62 @@ +package com.intuit.graphql.orchestrator.utils + +import graphql.language.Directive +import graphql.language.Field +import graphql.language.SelectionSet +import helpers.BaseIntegrationTestSpecification + +import static NodeUtils.removeDirectiveFromNode + +class NodeUtilsSpec extends BaseIntegrationTestSpecification { + def "removeDirectiveFromNode throws exception if node is null"(){ + when: + removeDirectiveFromNode(null, "") + then: + thrown(RuntimeException) + } + + def "removeDirectiveFromNode throws exception if node isn't a directiveContainer"(){ + when: + SelectionSet selectionSet = SelectionSet.newSelectionSet().build() + removeDirectiveFromNode(selectionSet, "") + then: + thrown(RuntimeException) + } + + def "removeDirectiveFromNode returns original node if directive isn't found"(){ + given: + Directive dir1 = Directive.newDirective().name("test1").build() + Directive dir2 = Directive.newDirective().name("test2").build() + Field testField = Field.newField("testField") + .directive(dir1) + .directive(dir2) + .build() + + when: + Field result = removeDirectiveFromNode(testField, "test3") + + then: + result.getName() == "testField" + result.getDirectives().size() == 2 + result.getDirectives().get(0).name == "test1" + result.getDirectives().get(1).name == "test2" + } + + def "removeDirectiveFromNode returns node without desired directive name"(){ + given: + Directive dir1 = Directive.newDirective().name("test1").build() + Directive dir2 = Directive.newDirective().name("test2").build() + Field testField = Field.newField("testField") + .directive(dir1) + .directive(dir2) + .build() + + when: + Field result = removeDirectiveFromNode(testField, "test1") + + then: + result.getName() == "testField" + result.getDirectives().size() == 1 + result.getDirectives().get(0).name == "test2" + } +} diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/visitors/DeferQueryExtractorSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/visitors/DeferQueryExtractorSpec.groovy new file mode 100644 index 00000000..7f4df563 --- /dev/null +++ b/src/test/groovy/com/intuit/graphql/orchestrator/visitors/DeferQueryExtractorSpec.groovy @@ -0,0 +1,693 @@ +package com.intuit.graphql.orchestrator.visitors + +import com.intuit.graphql.orchestrator.GraphQLOrchestrator +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions +import com.intuit.graphql.orchestrator.visitors.queryVisitors.DeferQueryExtractor +import graphql.ExecutionInput +import graphql.GraphQLException +import graphql.analysis.QueryTransformer +import graphql.language.Document +import graphql.language.Field +import graphql.language.FragmentDefinition +import graphql.language.OperationDefinition +import helpers.BaseIntegrationTestSpecification +import lombok.extern.slf4j.Slf4j + +import java.util.function.Function +import java.util.stream.Collectors + +import static com.intuit.graphql.orchestrator.utils.GraphQLUtil.parser +/** + * Covers test for ObjectTypeExtension, InterfaceTypeExtension, UnionTypeExtension, EnumTypeExtension, + * InputObjectTypeExtension TODO ScalarTypeExtension. + */ +@Slf4j +class DeferQueryExtractorSpec extends BaseIntegrationTestSpecification { + private static final DeferOptions deferOptions = DeferOptions.builder() + .nestedDefersAllowed(true) + .build() + private static final DeferOptions disabledDeferOptions = DeferOptions.builder() + .nestedDefersAllowed(false) + .build() + + private deferTestSchema = """ + type Query { + queryA: NestedObjectA + argQuery(id: String): NestedObjectA + } + + type NestedObjectA { + fieldA: String + fieldB: String + fieldC: String + objectField: TopLevelObject + } + + type TopLevelObject { + fieldD: String + fieldE: String + fieldF: String + nestedObject: NestedObject + } + + type NestedObject { + fieldG: String + fieldH: String + fieldI: String + } + """ + + private deferService = createSimpleMockService("DEFER", deferTestSchema, new HashMap()) + private GraphQLOrchestrator orchestrator = createGraphQLOrchestrator(deferService) + + def "can split Execution input"() { + given: + String query = "query { queryA { fieldA fieldB fieldC @defer } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 1 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " fieldC\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "can split EI with alias selections"(){ + given: + String query = "query { queryA { aliasA: fieldA aliasB: fieldB @defer } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " aliasB: fieldB\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "can split execution input with arguments"() { + given: + String query = "query { argQuery(id: \"inputA\") { fieldA fieldB @defer } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 1 + splitSet.get(0).query == "query {\n" + + " argQuery(id: \"inputA\") {\n" + + " fieldB\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "can split EI with multiple defer on same level"() { + given: + String query = "query { queryA { fieldA fieldB @defer fieldC @defer } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 2 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " fieldB\n" + + " __typename\n" + + " }\n" + + "}\n" + splitSet.get(1).query == "query {\n" + + " queryA {\n" + + " fieldC\n" + + " __typename\n" + + " }\n" + + "}\n" + + + } + + def "can split EI with deferred nested selections"() { + given: + String query = "query { queryA { fieldA objectField { fieldD fieldE @defer } } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " objectField {\n" + + " fieldE" + + "\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "Does not split EI when if arg is false"() { + given: + String query = "query { queryA { fieldA fieldB fieldC @defer(if: false) } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 0 + } + + def "prunes selections sets without fields after removing deferred fields"() { + given: + String query = "query { queryA { fieldA objectField { fieldD nestedObject @defer { fieldH @defer} } } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 1 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " objectField {\n" + + " nestedObject {\n" + + " fieldH\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "can split nested defer selections"() { + given: + String query = "query { queryA { fieldA objectField @defer { fieldD fieldE @defer } } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 2 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " objectField {\n" + + " fieldD\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + + splitSet.get(1).query == "query {\n" + + " queryA {\n" + + " objectField {\n" + + " fieldE\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "exception thrown for nested defer selections when option is off"() { + given: + String query = "query { queryA { fieldA objectField @defer { fieldD fieldE @defer } } }" + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(disabledDeferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + def exception = thrown(GraphQLException) + exception.getMessage() ==~ "Nested defers are currently unavailable." + } + + //todo + def "can split EI with variables" () {} + + def "can split EI with inline fragment"() { + given: + String query = """ + query { + queryA { + fieldA + objectField { + fieldD + } + ... on TopLevelObject @defer { + fieldE + } + } + } + """ + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 1 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " ... on TopLevelObject {\n" + + " fieldE\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "split EI has correct inline fragments different types"() { + given: + String query = """ + query { + queryA { + fieldA + objectField { + fieldD + } + ... on TopLevelObject @defer { + fieldF + } + objectField { + nestedObject { + fieldH + } + } + ... on TopLevelObject @defer { + fieldE + } + } + } + """ + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 2 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " ... on TopLevelObject {\n" + + " fieldF\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + splitSet.get(1).query == "query {\n" + + " queryA {\n" + + " ... on TopLevelObject {\n" + + " fieldE\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "split EI has correct inline fragments non merged types"() { + given: + String query = """ + query { + queryA { + fieldA + objectField { + fieldD + } + ... on TopLevelObject @defer { + fieldF + } + objectField { + fieldE + } + ... on TopLevelObject @defer { + fieldE + } + } + } + """ + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(new HashMap()) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 2 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " ... on TopLevelObject {\n" + + " fieldF\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + splitSet.get(1).query == "query {\n" + + " queryA {\n" + + " ... on TopLevelObject {\n" + + " fieldE\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + } + + def "can split EI with fragment spread"() { + given: + String query = """ + query { + queryA { + fieldA + objectField { + fieldD + ... deferredInfo @defer + } + } + } + fragment deferredInfo on TopLevelObject { + fieldE + } + """ + + ExecutionInput ei = ExecutionInput.newExecutionInput(query).build() + Document rootDocument = parser.parseDocument(query) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + Field selection = opDef.selectionSet.getSelections().get(0) as Field + + when: + Map fragmentDefinitionMap = rootDocument.getDefinitionsOfType(FragmentDefinition.class) + .stream() + .collect(Collectors.toMap({ fragment -> ((FragmentDefinition)fragment).getName() }, Function.identity())); + + DeferQueryExtractor visitor = DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .fragmentDefinitionMap(fragmentDefinitionMap) + .build() + + QueryTransformer.newQueryTransformer() + .schema(orchestrator.getSchema()) + .rootParentType(orchestrator.getSchema().getQueryType()) + .root(selection) + .fragmentsByName(fragmentDefinitionMap) + .variables(ei.getVariables()) + .build() + .transform(visitor) + + then: + List splitSet = visitor.getExtractedEIs() + splitSet.size() == 1 + splitSet.get(0).query == "query {\n" + + " queryA {\n" + + " objectField {\n" + + " ...deferredInfo\n" + + " __typename\n" + + " }\n" + + " __typename\n" + + " }\n" + + "}\n" + + "\nfragment deferredInfo on TopLevelObject {\n" + + " fieldE\n" + + "}\n" + } + + def "thrown exception when building with null defer options"(){ + given: + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("query { queryA { fieldA } }") + .build() + Document rootDocument = parser.parseDocument(ei.getQuery()) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + + when: + DeferQueryExtractor.builder() + .originalEI(ei) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(null) + .build() + + then: + thrown(NullPointerException) + } + + def "thrown exception when building with null ei"(){ + given: + ExecutionInput ei = ExecutionInput.newExecutionInput() + .query("query { queryA { fieldA } }") + .build() + Document rootDocument = parser.parseDocument(ei.getQuery()) + OperationDefinition opDef = rootDocument.getFirstDefinitionOfType(OperationDefinition).get() + + when: + DeferQueryExtractor.builder() + .originalEI(null) + .rootNode(rootDocument) + .operationDefinition(opDef) + .deferOptions(deferOptions) + .build() + + then: + thrown(NullPointerException) + } +} diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/visitors/PruneChildDeferSelectionsModifierSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/visitors/PruneChildDeferSelectionsModifierSpec.groovy new file mode 100644 index 00000000..60fb7f99 --- /dev/null +++ b/src/test/groovy/com/intuit/graphql/orchestrator/visitors/PruneChildDeferSelectionsModifierSpec.groovy @@ -0,0 +1,294 @@ +package com.intuit.graphql.orchestrator.visitors + +import com.intuit.graphql.orchestrator.deferDirective.DeferOptions +import com.intuit.graphql.orchestrator.visitors.queryVisitors.PruneChildDeferSelectionsModifier +import graphql.GraphQLException +import graphql.language.* +import lombok.extern.slf4j.Slf4j +import spock.lang.Specification + +@Slf4j +class PruneChildDeferSelectionsModifierSpec extends Specification { + + Directive enabledDirective = Directive.newDirective() + .name("defer") + .argument(Argument.newArgument("if", BooleanValue.of(true)).build()) + .build() + Directive disabledDirective = Directive.newDirective() + .name("defer") + .argument(Argument.newArgument("if", BooleanValue.of(false)).build()) + .build() + + DeferOptions enabledNestedDefer = DeferOptions.builder().nestedDefersAllowed(true).build() + DeferOptions disabledNestedDefer = DeferOptions.builder().nestedDefersAllowed(false).build() + + PruneChildDeferSelectionsModifier specToTest = PruneChildDeferSelectionsModifier.builder() + .deferOptions(disabledNestedDefer) + .build() + + PruneChildDeferSelectionsModifier nestedSpecToTest = PruneChildDeferSelectionsModifier.builder() + .deferOptions(enabledNestedDefer) + .build() + + AstTransformer astTransformer = new AstTransformer() + + def "Top Level Deferred Child Field is Removed"(){ + given: + Field deferredField = Field.newField("topLevelDeferredChild").directive(enabledDirective).build() + Field childField2 = Field.newField("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredField).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 2 + ((Field)result.getSelectionSet().getSelections().get(0)).getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "__typename" + } + + def "Nested Deferred Child Field is Removed"(){ + given: + Field deferredField = Field.newField("nestedDeferredChild").directive(enabledDirective).build() + Field childField2 = Field.newField("nestedChild").build() + SelectionSet nestedSelectionSet = SelectionSet.newSelectionSet().selection(deferredField).selection(childField2).build() + Field topLevelChild = Field.newField("topLevelChild", nestedSelectionSet).build() + + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(topLevelChild).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 2 + + Field resultTopChild = (Field)result.getSelectionSet().getSelections().get(0) + topLevelChild.getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "__typename" + + resultTopChild.getSelectionSet().getSelections().size() == 2 + ((Field) resultTopChild.getSelectionSet().getSelections().get(0)).getName() == "nestedChild" + ((Field) resultTopChild.getSelectionSet().getSelections().get(1)).getName() == "__typename" + } + + def "Removes Disabled Defer Directive From Child Field"(){ + given: + Field deferredField = Field.newField("topLevelDeferredChild").directive(disabledDirective).build() + Field childField2 = Field.newField("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredField).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 3 + ((Field)result.getSelectionSet().getSelections().get(0)).getName() == "topLevelDeferredChild" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(2)).getName() == "__typename" + } + + def "Throws exception if Field Has Defer Directive and nestedDefer is not Allowed"(){ + given: + Field deferredField = Field.newField("topLevelDeferredChild").directive(enabledDirective).build() + Field childField2 = Field.newField("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredField).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + astTransformer.transform(root, specToTest) + + then: + def exception = thrown(GraphQLException) + exception.getMessage() ==~ "Nested defers are currently unavailable." + } + + def "Top Level Deferred Child FragmentSpread is Removed"(){ + given: + FragmentSpread deferredFragment = FragmentSpread.newFragmentSpread("topLevelFragment") + .directive(enabledDirective) + .build() + Field childField2 = Field.newField().name("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 2 + ((Field)result.getSelectionSet().getSelections().get(0)).getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "__typename" + } + + def "Nested Deferred Child FragmentSpread is Removed"(){ + given: + FragmentSpread deferredFragment = FragmentSpread.newFragmentSpread("nestedDeferredChild") + .directive(enabledDirective) + .build() + Field childField2 = Field.newField().name("nestedChild").build() + SelectionSet nestedSelectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field topLevelChild = Field.newField("topLevelChild", nestedSelectionSet).build() + + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(topLevelChild).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 2 + + Field resultTopChild = (Field)result.getSelectionSet().getSelections().get(0) + topLevelChild.getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "__typename" + + resultTopChild.getSelectionSet().getSelections().size() == 2 + ((Field) resultTopChild.getSelectionSet().getSelections().get(0)).getName() == "nestedChild" + ((Field) resultTopChild.getSelectionSet().getSelections().get(1)).getName() == "__typename" + + } + + def "Removes Disabled Defer Directive From Child FragmentSpread"(){ + given: + FragmentSpread deferredFragment = FragmentSpread.newFragmentSpread("DeferredFrag") + .directive(disabledDirective) + .build() + Field childField2 = Field.newField("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 3 + ((FragmentSpread)result.getSelectionSet().getSelections().get(0)).getName() == "DeferredFrag" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(2)).getName() == "__typename" + } + + def "Throws exception if FragmentSpread Has Defer Directive and nestedDefer is not Allowed"(){ + given: + FragmentSpread deferredFragment = FragmentSpread.newFragmentSpread("topLevelFragment") + .directive(enabledDirective) + .build() + Field childField2 = Field.newField().name("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + astTransformer.transform(root, specToTest) + + then: + def exception = thrown(GraphQLException) + exception.getMessage() ==~ "Nested defers are currently unavailable." + } + + def "Top Level Deferred Child InlineFragment is Removed"(){ + given: + SelectionSet deferredSelectionSet = SelectionSet.newSelectionSet() + .selection(Field.newField("deferredFragField").build()) + .build() + InlineFragment deferredFragment = InlineFragment.newInlineFragment().selectionSet(deferredSelectionSet) + .directive(enabledDirective) + .build() + Field childField2 = Field.newField().name("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 2 + ((Field)result.getSelectionSet().getSelections().get(0)).getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "__typename" + } + + def "Nested Deferred Child InlineFragment is Removed"(){ + given: + SelectionSet deferredSelectionSet = SelectionSet.newSelectionSet() + .selection(Field.newField("deferredFragField").build()) + .build() + InlineFragment deferredFragment = InlineFragment.newInlineFragment().selectionSet(deferredSelectionSet) + .directive(enabledDirective) + .build() + Field childField2 = Field.newField().name("nestedChild").build() + SelectionSet nestedSelectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field topLevelChild = Field.newField("topLevelChild", nestedSelectionSet).build() + + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(topLevelChild).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 2 + + Field resultTopChild = (Field)result.getSelectionSet().getSelections().get(0) + topLevelChild.getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "__typename" + + resultTopChild.getSelectionSet().getSelections().size() == 2 + ((Field) resultTopChild.getSelectionSet().getSelections().get(0)).getName() == "nestedChild" + ((Field) resultTopChild.getSelectionSet().getSelections().get(1)).getName() == "__typename" + + } + + def "Removes Disabled Defer Directive From Child InlineFragment"(){ + given: + SelectionSet deferredSelectionSet = SelectionSet.newSelectionSet() + .selection(Field.newField("deferredFragField").build()) + .build() + InlineFragment deferredFragment = InlineFragment.newInlineFragment().selectionSet(deferredSelectionSet) + .directive(disabledDirective) + .build() + Field childField2 = Field.newField("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + Field result = (Field) astTransformer.transform(root, nestedSpecToTest) + + then: + result != null + result.getSelectionSet().getSelections().size() == 3 + InlineFragment fragment = (InlineFragment)result.getSelectionSet().getSelections().get(0) + ((Field)fragment.getSelectionSet().getSelections().get(0)).getName() == "deferredFragField" + ((Field)fragment.getSelectionSet().getSelections().get(1)).getName() == "__typename" + ((Field)result.getSelectionSet().getSelections().get(1)).getName() == "topLevelChild" + ((Field)result.getSelectionSet().getSelections().get(2)).getName() == "__typename" + } + + def "Throws exception if InlineFragment Has Defer Directive and nestedDefer is not Allowed"(){ + given: + SelectionSet deferredSelectionSet = SelectionSet.newSelectionSet() + .selection(Field.newField("deferredFragField").build()) + .build() + InlineFragment deferredFragment = InlineFragment.newInlineFragment().selectionSet(deferredSelectionSet) + .directive(enabledDirective) + .build() + Field childField2 = Field.newField().name("topLevelChild").build() + SelectionSet selectionSet = SelectionSet.newSelectionSet().selection(deferredFragment).selection(childField2).build() + Field root = Field.newField("rootField", selectionSet).build() + + when: + astTransformer.transform(root, specToTest) + + then: + def exception = thrown(GraphQLException) + exception.getMessage() ==~ "Nested defers are currently unavailable." + } +} diff --git a/src/test/groovy/helpers/BaseIntegrationTestSpecification.groovy b/src/test/groovy/helpers/BaseIntegrationTestSpecification.groovy index c2d69116..bd52c8be 100644 --- a/src/test/groovy/helpers/BaseIntegrationTestSpecification.groovy +++ b/src/test/groovy/helpers/BaseIntegrationTestSpecification.groovy @@ -10,6 +10,7 @@ import graphql.ExecutionInput import graphql.execution.AsyncExecutionStrategy import graphql.execution.ExecutionIdProvider import graphql.execution.ExecutionStrategy +import graphql.language.AstTransformer import graphql.language.Document import graphql.language.OperationDefinition import graphql.parser.Parser @@ -18,6 +19,7 @@ import spock.lang.Specification class BaseIntegrationTestSpecification extends Specification { public static final Parser PARSER = new Parser() + public static final AstTransformer AST_TRANSFORMER = new AstTransformer() def testService