Skip to content

Commit b1eaa12

Browse files
committed
extract specific query transformer from QueryTraversal
1 parent ecf8a08 commit b1eaa12

4 files changed

Lines changed: 377 additions & 209 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package graphql.analysis;
2+
3+
import graphql.Internal;
4+
import graphql.execution.ConditionalNodes;
5+
import graphql.execution.ValuesResolver;
6+
import graphql.introspection.Introspection;
7+
import graphql.language.Field;
8+
import graphql.language.FragmentDefinition;
9+
import graphql.language.FragmentSpread;
10+
import graphql.language.InlineFragment;
11+
import graphql.language.Node;
12+
import graphql.language.NodeTraverser;
13+
import graphql.language.NodeVisitorStub;
14+
import graphql.language.TypeName;
15+
import graphql.schema.GraphQLCodeRegistry;
16+
import graphql.schema.GraphQLCompositeType;
17+
import graphql.schema.GraphQLFieldDefinition;
18+
import graphql.schema.GraphQLFieldsContainer;
19+
import graphql.schema.GraphQLSchema;
20+
import graphql.schema.GraphQLUnmodifiedType;
21+
import graphql.util.TraversalControl;
22+
import graphql.util.TraverserContext;
23+
24+
import java.util.Map;
25+
26+
import static graphql.Assert.assertNotNull;
27+
import static graphql.language.NodeTraverser.LeaveOrEnter.LEAVE;
28+
import static graphql.schema.GraphQLTypeUtil.unwrapAll;
29+
30+
/**
31+
* Internally used node visitor which delegates to a {@link QueryVisitor} with type
32+
* information about the visited field.
33+
*/
34+
@Internal
35+
public class NodeVisitorWithTypeTracking extends NodeVisitorStub {
36+
37+
38+
private final QueryVisitor preOrderCallback;
39+
private final QueryVisitor postOrderCallback;
40+
private final Map<String, Object> variables;
41+
private final GraphQLSchema schema;
42+
private final Map<String, FragmentDefinition> fragmentsByName;
43+
44+
private final ConditionalNodes conditionalNodes = new ConditionalNodes();
45+
private final ValuesResolver valuesResolver = new ValuesResolver();
46+
47+
48+
public NodeVisitorWithTypeTracking(QueryVisitor preOrderCallback, QueryVisitor postOrderCallback, Map<String, Object> variables, GraphQLSchema schema, Map<String, FragmentDefinition> fragmentsByName) {
49+
this.preOrderCallback = preOrderCallback;
50+
this.postOrderCallback = postOrderCallback;
51+
this.variables = variables;
52+
this.schema = schema;
53+
this.fragmentsByName = fragmentsByName;
54+
}
55+
56+
@Override
57+
public TraversalControl visitInlineFragment(InlineFragment inlineFragment, TraverserContext<Node> context) {
58+
if (!conditionalNodes.shouldInclude(variables, inlineFragment.getDirectives())) {
59+
return TraversalControl.ABORT;
60+
}
61+
62+
QueryVisitorInlineFragmentEnvironment inlineFragmentEnvironment = new QueryVisitorInlineFragmentEnvironmentImpl(inlineFragment, context);
63+
64+
if (context.getVar(NodeTraverser.LeaveOrEnter.class) == LEAVE) {
65+
postOrderCallback.visitInlineFragment(inlineFragmentEnvironment);
66+
return TraversalControl.CONTINUE;
67+
}
68+
69+
preOrderCallback.visitInlineFragment(inlineFragmentEnvironment);
70+
71+
// inline fragments are allowed not have type conditions, if so the parent type counts
72+
QueryTraversalContext parentEnv = context.getVarFromParents(QueryTraversalContext.class);
73+
74+
GraphQLCompositeType fragmentCondition;
75+
if (inlineFragment.getTypeCondition() != null) {
76+
TypeName typeCondition = inlineFragment.getTypeCondition();
77+
fragmentCondition = (GraphQLCompositeType) schema.getType(typeCondition.getName());
78+
} else {
79+
fragmentCondition = parentEnv.getRawType();
80+
}
81+
// for unions we only have other fragments inside
82+
context.setVar(QueryTraversalContext.class, new QueryTraversalContext(fragmentCondition, fragmentCondition, parentEnv.getEnvironment(), inlineFragment));
83+
return TraversalControl.CONTINUE;
84+
}
85+
86+
@Override
87+
public TraversalControl visitFragmentDefinition(FragmentDefinition node, TraverserContext<Node> context) {
88+
if (!conditionalNodes.shouldInclude(variables, node.getDirectives())) {
89+
return TraversalControl.ABORT;
90+
}
91+
92+
if (context.getVar(NodeTraverser.LeaveOrEnter.class) == LEAVE) {
93+
return TraversalControl.CONTINUE;
94+
}
95+
96+
97+
QueryTraversalContext parentEnv = context.getVarFromParents(QueryTraversalContext.class);
98+
GraphQLCompositeType typeCondition = (GraphQLCompositeType) schema.getType(node.getTypeCondition().getName());
99+
context
100+
.setVar(QueryTraversalContext.class, new QueryTraversalContext(typeCondition, typeCondition, parentEnv.getEnvironment(), node));
101+
return TraversalControl.CONTINUE;
102+
}
103+
104+
@Override
105+
public TraversalControl visitFragmentSpread(FragmentSpread fragmentSpread, TraverserContext<Node> context) {
106+
if (!conditionalNodes.shouldInclude(variables, fragmentSpread.getDirectives())) {
107+
return TraversalControl.ABORT;
108+
}
109+
110+
FragmentDefinition fragmentDefinition = fragmentsByName.get(fragmentSpread.getName());
111+
if (!conditionalNodes.shouldInclude(variables, fragmentDefinition.getDirectives())) {
112+
return TraversalControl.ABORT;
113+
}
114+
115+
QueryVisitorFragmentSpreadEnvironment fragmentSpreadEnvironment = new QueryVisitorFragmentSpreadEnvironmentImpl(fragmentSpread, fragmentDefinition, context);
116+
if (context.getVar(NodeTraverser.LeaveOrEnter.class) == LEAVE) {
117+
postOrderCallback.visitFragmentSpread(fragmentSpreadEnvironment);
118+
return TraversalControl.CONTINUE;
119+
}
120+
121+
preOrderCallback.visitFragmentSpread(fragmentSpreadEnvironment);
122+
123+
QueryTraversalContext parentEnv = context.getVarFromParents(QueryTraversalContext.class);
124+
125+
GraphQLCompositeType typeCondition = (GraphQLCompositeType) schema.getType(fragmentDefinition.getTypeCondition().getName());
126+
assertNotNull(typeCondition, "Invalid type condition '%s' in fragment '%s'", fragmentDefinition.getTypeCondition().getName(),
127+
fragmentDefinition.getName());
128+
context
129+
.setVar(QueryTraversalContext.class, new QueryTraversalContext(typeCondition, typeCondition, parentEnv.getEnvironment(), fragmentDefinition));
130+
return TraversalControl.CONTINUE;
131+
}
132+
133+
@Override
134+
public TraversalControl visitField(Field field, TraverserContext<Node> context) {
135+
QueryTraversalContext parentEnv = context.getVarFromParents(QueryTraversalContext.class);
136+
137+
GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(schema, parentEnv.getRawType(), field.getName());
138+
boolean isTypeNameIntrospectionField = fieldDefinition == Introspection.TypeNameMetaFieldDef;
139+
GraphQLFieldsContainer fieldsContainer = !isTypeNameIntrospectionField ? (GraphQLFieldsContainer) unwrapAll(parentEnv.getOutputType()) : null;
140+
GraphQLCodeRegistry codeRegistry = schema.getCodeRegistry();
141+
Map<String, Object> argumentValues = valuesResolver.getArgumentValues(codeRegistry, fieldDefinition.getArguments(), field.getArguments(), variables);
142+
QueryVisitorFieldEnvironment environment = new QueryVisitorFieldEnvironmentImpl(isTypeNameIntrospectionField,
143+
field,
144+
fieldDefinition,
145+
parentEnv.getOutputType(),
146+
fieldsContainer,
147+
parentEnv.getEnvironment(),
148+
argumentValues,
149+
parentEnv.getSelectionSetContainer(),
150+
context);
151+
152+
NodeTraverser.LeaveOrEnter leaveOrEnter = context.getVar(NodeTraverser.LeaveOrEnter.class);
153+
if (leaveOrEnter == LEAVE) {
154+
postOrderCallback.visitField(environment);
155+
return TraversalControl.CONTINUE;
156+
}
157+
158+
if (!conditionalNodes.shouldInclude(variables, field.getDirectives())) {
159+
return TraversalControl.ABORT;
160+
}
161+
162+
preOrderCallback.visitField(environment);
163+
164+
GraphQLUnmodifiedType unmodifiedType = unwrapAll(fieldDefinition.getType());
165+
QueryTraversalContext fieldEnv = (unmodifiedType instanceof GraphQLCompositeType)
166+
? new QueryTraversalContext(fieldDefinition.getType(), (GraphQLCompositeType) unmodifiedType, environment, field)
167+
: new QueryTraversalContext(null, null, environment, field);// Terminal (scalar) node, EMPTY FRAME
168+
169+
170+
context.setVar(QueryTraversalContext.class, fieldEnv);
171+
return TraversalControl.CONTINUE;
172+
}
173+
174+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package graphql.analysis;
2+
3+
import graphql.PublicApi;
4+
import graphql.language.FragmentDefinition;
5+
import graphql.language.Node;
6+
import graphql.language.NodeTraverser.LeaveOrEnter;
7+
import graphql.schema.GraphQLObjectType;
8+
import graphql.schema.GraphQLSchema;
9+
import graphql.util.TraversalControl;
10+
import graphql.util.TraverserContext;
11+
import graphql.util.TraverserVisitor;
12+
import graphql.util.TreeTransformer;
13+
14+
import java.util.LinkedHashMap;
15+
import java.util.Map;
16+
17+
import static graphql.Assert.assertNotNull;
18+
import static graphql.language.AstNodeAdapter.AST_NODE_ADAPTER;
19+
20+
/**
21+
* Helps to transform a Document (or parts of it) and tracks at the same time the corresponding Schema types.
22+
* <p>
23+
* This is an important distinction to just traversing the Document without any type information: Each field has a clearly
24+
* defined type. See {@link QueryVisitorFieldEnvironment}.
25+
* <p>
26+
* Further are the built in Directives skip/include automatically evaluated: if parts of the Document should be ignored they will not
27+
* be visited. But this is not a full evaluation of a Query: every fragment will be visited/followed regardless of the type condition.
28+
* <p>
29+
* It also doesn't consider field merging, which means for example {@code { user{firstName} user{firstName}} } will result in four
30+
* visitField calls.
31+
*/
32+
@PublicApi
33+
public class QueryTransformer {
34+
35+
private final Node root;
36+
private final GraphQLSchema schema;
37+
private final Map<String, FragmentDefinition> fragmentsByName;
38+
private final Map<String, Object> variables;
39+
40+
private final GraphQLObjectType rootParentType;
41+
42+
43+
private QueryTransformer(GraphQLSchema schema,
44+
Node root,
45+
GraphQLObjectType rootParentType,
46+
Map<String, FragmentDefinition> fragmentsByName,
47+
Map<String, Object> variables) {
48+
this.schema = assertNotNull(schema, "schema can't be null");
49+
this.variables = assertNotNull(variables, "variables can't be null");
50+
this.root = assertNotNull(root, "root can't be null");
51+
this.rootParentType = assertNotNull(rootParentType);
52+
this.fragmentsByName = assertNotNull(fragmentsByName, "fragmentsByName can't be null");
53+
}
54+
55+
/**
56+
* Visits the Document in pre-order and allows to transform it using {@link graphql.util.TreeTransformerUtil}
57+
* methods. Please note that fragment spreads are not followed and need to be
58+
* processed explicitly by supplying them as root.
59+
*
60+
* @param queryVisitor the query visitor that will be called back.
61+
*
62+
* @return changed root
63+
*
64+
* @throws IllegalArgumentException if there are multiple root nodes.
65+
*/
66+
public Node transform(QueryVisitor queryVisitor) {
67+
QueryVisitor noOp = new QueryVisitorStub();
68+
NodeVisitorWithTypeTracking nodeVisitor = new NodeVisitorWithTypeTracking(queryVisitor, noOp, variables, schema, fragmentsByName);
69+
70+
Map<Class<?>, Object> rootVars = new LinkedHashMap<>();
71+
rootVars.put(QueryTraversalContext.class, new QueryTraversalContext(rootParentType, rootParentType, null, null));
72+
73+
TraverserVisitor<Node> nodeTraverserVisitor = new TraverserVisitor<Node>() {
74+
75+
@Override
76+
public TraversalControl enter(TraverserContext<Node> context) {
77+
context.setVar(LeaveOrEnter.class, LeaveOrEnter.ENTER);
78+
return context.thisNode().accept(context, nodeVisitor);
79+
}
80+
81+
@Override
82+
public TraversalControl leave(TraverserContext<Node> context) {
83+
//Transformations are applied preOrder only
84+
return TraversalControl.CONTINUE;
85+
}
86+
};
87+
return new TreeTransformer<>(AST_NODE_ADAPTER).transform(root, nodeTraverserVisitor, rootVars);
88+
}
89+
90+
public static Builder newQueryTransformer() {
91+
return new Builder();
92+
}
93+
94+
@PublicApi
95+
public static class Builder {
96+
private GraphQLSchema schema;
97+
private Map<String, Object> variables;
98+
99+
private Node root;
100+
private GraphQLObjectType rootParentType;
101+
private Map<String, FragmentDefinition> fragmentsByName;
102+
103+
104+
/**
105+
* The schema used to identify the types of the query.
106+
*
107+
* @param schema the schema to use
108+
*
109+
* @return this builder
110+
*/
111+
public Builder schema(GraphQLSchema schema) {
112+
this.schema = schema;
113+
return this;
114+
}
115+
116+
/**
117+
* Variables used in the query.
118+
*
119+
* @param variables the variables to use
120+
*
121+
* @return this builder
122+
*/
123+
public Builder variables(Map<String, Object> variables) {
124+
this.variables = variables;
125+
return this;
126+
}
127+
128+
/**
129+
* Specify the root node for the transformation.
130+
*
131+
* @param root the root node to use
132+
*
133+
* @return this builder
134+
*/
135+
public Builder root(Node root) {
136+
this.root = root;
137+
return this;
138+
}
139+
140+
/**
141+
* The type of the parent of the root node. (See {@link Builder#root(Node)}
142+
*
143+
* @param rootParentType the root parent type
144+
*
145+
* @return this builder
146+
*/
147+
public Builder rootParentType(GraphQLObjectType rootParentType) {
148+
this.rootParentType = rootParentType;
149+
return this;
150+
}
151+
152+
/**
153+
* Fragment by name map. Needs to be provided together with a {@link Builder#root(Node)} and {@link Builder#rootParentType(GraphQLObjectType)}
154+
*
155+
* @param fragmentsByName the map of fragments
156+
*
157+
* @return this builder
158+
*/
159+
public Builder fragmentsByName(Map<String, FragmentDefinition> fragmentsByName) {
160+
this.fragmentsByName = fragmentsByName;
161+
return this;
162+
}
163+
164+
/**
165+
* @return a built {@link QueryTransformer} object
166+
*/
167+
public QueryTransformer build() {
168+
return new QueryTransformer(schema, root, rootParentType, fragmentsByName, variables);
169+
}
170+
171+
}
172+
}

0 commit comments

Comments
 (0)