Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix double variable coercion in QueryTraverser #2836

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ QueryTraverser newQueryTraverser(ExecutionContext executionContext) {
.schema(executionContext.getGraphQLSchema())
.document(executionContext.getDocument())
.operationName(executionContext.getExecutionInput().getOperationName())
.variables(executionContext.getVariables())
.coercedVariables(executionContext.getCoercedVariables())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love it!

.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ QueryTraverser newQueryTraverser(ExecutionContext executionContext) {
.schema(executionContext.getGraphQLSchema())
.document(executionContext.getDocument())
.operationName(executionContext.getExecutionInput().getOperationName())
.variables(executionContext.getVariables())
.coercedVariables(executionContext.getCoercedVariables())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

.build();
}

Expand Down
76 changes: 53 additions & 23 deletions src/main/java/graphql/analysis/QueryTraverser.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,38 @@ public class QueryTraverser {
private QueryTraverser(GraphQLSchema schema,
Document document,
String operation,
Map<String, Object> variables) {
assertNotNull(document, () -> "document can't be null");
CoercedVariables coercedVariables) {
this.schema = schema;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you get rid of assertNotNull(document, () -> "document can't be null"); accidentilly here??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably saw it later - the null assert is inside the builder now

NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operation);
List<VariableDefinition> variableDefinitions = getOperationResult.operationDefinition.getVariableDefinitions();
this.schema = assertNotNull(schema, () -> "schema can't be null");
this.fragmentsByName = getOperationResult.fragmentsByName;
this.roots = singletonList(getOperationResult.operationDefinition);
this.rootParentType = getRootTypeFromOperation(getOperationResult.operationDefinition);
this.coercedVariables = coerceVariables(assertNotNull(variables, () -> "variables can't be null"), variableDefinitions);
this.coercedVariables = coercedVariables;
}

private CoercedVariables coerceVariables(Map<String, Object> rawVariables, List<VariableDefinition> variableDefinitions) {
return new ValuesResolver().coerceVariableValues(schema, variableDefinitions, new RawVariables(rawVariables));
private QueryTraverser(GraphQLSchema schema,
Document document,
String operation,
RawVariables rawVariables) {
this.schema = schema;
NodeUtil.GetOperationResult getOperationResult = NodeUtil.getOperation(document, operation);
List<VariableDefinition> variableDefinitions = getOperationResult.operationDefinition.getVariableDefinitions();
this.fragmentsByName = getOperationResult.fragmentsByName;
this.roots = singletonList(getOperationResult.operationDefinition);
this.rootParentType = getRootTypeFromOperation(getOperationResult.operationDefinition);
this.coercedVariables = new ValuesResolver().coerceVariableValues(schema, variableDefinitions, rawVariables);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one for Andi omre yhan you - but since ValuesResolver is a stateless class - we should move to static entry points. Object allocation is cheap - but statics are even cheaper AND it says its a singleton

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, doesn't make sense to have an instance here.

I'll have a chat to Andi and make this change in a separate PR

}

private QueryTraverser(GraphQLSchema schema,
Node root,
GraphQLCompositeType rootParentType,
Map<String, FragmentDefinition> fragmentsByName,
Map<String, Object> variables) {
this.schema = assertNotNull(schema, () -> "schema can't be null");
assertNotNull(root, () -> "root can't be null");
CoercedVariables coercedVariables) {
this.schema = schema;
this.roots = Collections.singleton(root);
this.rootParentType = assertNotNull(rootParentType, () -> "rootParentType can't be null");
this.fragmentsByName = assertNotNull(fragmentsByName, () -> "fragmentsByName can't be null");
this.coercedVariables = new CoercedVariables(assertNotNull(variables, () -> "variables can't be null"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the asserts removed because these invariants are no longer true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding 2 new constructors so to keep the code tidier I thought I would move all the null checks into the builder, rather than having to remember 4 lots of asserts

this.rootParentType = rootParentType;
this.fragmentsByName = fragmentsByName;
this.coercedVariables = coercedVariables;
}

public Object visitDepthFirst(QueryVisitor queryVisitor) {
Expand Down Expand Up @@ -196,7 +202,8 @@ public static class Builder {
private GraphQLSchema schema;
private Document document;
private String operation;
private Map<String, Object> variables;
private CoercedVariables coercedVariables = CoercedVariables.emptyVariables();
private RawVariables rawVariables;

private Node root;
private GraphQLCompositeType rootParentType;
Expand All @@ -211,7 +218,7 @@ public static class Builder {
* @return this builder
*/
public Builder schema(GraphQLSchema schema) {
this.schema = schema;
this.schema = assertNotNull(schema, () -> "schema can't be null");
return this;
}

Expand All @@ -237,19 +244,33 @@ public Builder operationName(String operationName) {
* @return this builder
*/
public Builder document(Document document) {
this.document = document;
this.document = assertNotNull(document, () -> "document can't be null");
return this;
}

/**
* Variables used in the query.
* Raw variables used in the query.
*
* @param variables the variables to use
*
* @return this builder
*/
public Builder variables(Map<String, Object> variables) {
this.variables = variables;
assertNotNull(variables, () -> "variables can't be null");
this.rawVariables = new RawVariables(variables);
return this;
}

/**
* Variables (already coerced) used in the query.
*
* @param coercedVariables the variables to use
*
* @return this builder
*/
public Builder coercedVariables(CoercedVariables coercedVariables) {
assertNotNull(coercedVariables, () -> "coercedVariables can't be null");
this.coercedVariables = coercedVariables;
return this;
}

Expand All @@ -262,7 +283,7 @@ public Builder variables(Map<String, Object> variables) {
* @return this builder
*/
public Builder root(Node root) {
this.root = root;
this.root = assertNotNull(root, () -> "root can't be null");
return this;
}

Expand All @@ -274,7 +295,7 @@ public Builder root(Node root) {
* @return this builder
*/
public Builder rootParentType(GraphQLCompositeType rootParentType) {
this.rootParentType = rootParentType;
this.rootParentType = assertNotNull(rootParentType, () -> "rootParentType can't be null");
return this;
}

Expand All @@ -286,7 +307,7 @@ public Builder rootParentType(GraphQLCompositeType rootParentType) {
* @return this builder
*/
public Builder fragmentsByName(Map<String, FragmentDefinition> fragmentsByName) {
this.fragmentsByName = fragmentsByName;
this.fragmentsByName = assertNotNull(fragmentsByName, () -> "fragmentsByName can't be null");
return this;
}

Expand All @@ -296,9 +317,18 @@ public Builder fragmentsByName(Map<String, FragmentDefinition> fragmentsByName)
public QueryTraverser build() {
checkState();
if (document != null) {
return new QueryTraverser(schema, document, operation, variables);
if (rawVariables != null) {
return new QueryTraverser(schema, document, operation, rawVariables);
}
return new QueryTraverser(schema, document, operation, coercedVariables);
} else {
return new QueryTraverser(schema, root, rootParentType, fragmentsByName, variables);
if (rawVariables != null) {
// When traversing with an arbitrary root, there is no variable definition context available
// Thus, the variables must have already been coerced
// Retaining this builder for backwards compatibility
return new QueryTraverser(schema, root, rootParentType, fragmentsByName, new CoercedVariables(rawVariables.toMap()));
}
return new QueryTraverser(schema, root, rootParentType, fragmentsByName, coercedVariables);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ static List<GraphQLError> validateFieldsAndArguments(FieldValidation fieldValida
.schema(executionContext.getGraphQLSchema())
.document(executionContext.getDocument())
.operationName(executionContext.getOperationDefinition().getName())
.variables(executionContext.getVariables())
.coercedVariables(executionContext.getCoercedVariables())
.build();

queryTraverser.visitPreOrder(new QueryVisitorStub() {
Expand Down
117 changes: 110 additions & 7 deletions src/test/groovy/graphql/analysis/QueryTraverserTest.groovy
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package graphql.analysis

import graphql.AssertException
import graphql.TestUtil
import graphql.execution.CoercedVariables
import graphql.language.ArrayValue
import graphql.language.Document
import graphql.language.Field
Expand Down Expand Up @@ -428,7 +430,7 @@ class QueryTraverserTest extends Specification {
}


def "test preOrder and postOrder order for fragment definitions"() {
def "test preOrder and postOrder order for fragment definitions and raw variables"() {
given:
def schema = TestUtil.schema("""
type Query{
Expand Down Expand Up @@ -475,6 +477,53 @@ class QueryTraverserTest extends Specification {
1 * visitor.visitFragmentDefinition({ QueryVisitorFragmentDefinitionEnvironment env -> env.fragmentDefinition == fragments["F1"] })
}

def "test preOrder and postOrder order for fragment definitions and coerced variables"() {
given:
def schema = TestUtil.schema("""
type Query{
foo: Foo
bar: String
}
type Foo {
subFoo: String
}
""")
def visitor = mockQueryVisitor()
def query = createQuery("""
{
...F1
}

fragment F1 on Query {
foo {
subFoo
}
}
""")

def fragments = NodeUtil.getFragmentsByName(query)

QueryTraverser queryTraversal = QueryTraverser.newQueryTraverser()
.schema(schema)
.root(fragments["F1"])
.rootParentType(schema.getQueryType())
.fragmentsByName(fragments)
.coercedVariables(CoercedVariables.emptyVariables())
.build()

when:
queryTraversal.visitPreOrder(visitor)

then:
1 * visitor.visitFragmentDefinition({ QueryVisitorFragmentDefinitionEnvironment env -> env.fragmentDefinition == fragments["F1"] })

when:
queryTraversal.visitPostOrder(visitor)

then:
1 * visitor.visitFragmentDefinition({ QueryVisitorFragmentDefinitionEnvironment env -> env.fragmentDefinition == fragments["F1"] })
}

def "works for mutations()"() {
given:
def schema = TestUtil.schema("""
Expand Down Expand Up @@ -1378,7 +1427,9 @@ class QueryTraverserTest extends Specification {
}


def "can select an arbitrary root node"() {
def "can select an arbitrary root node with coerced variables as plain map"() {
// When using an arbitrary root node, there is no variable definition context available.
// Thus the variables must have already been coerced, but may appear as a plain map rather than CoercedVariables
given:
def schema = TestUtil.schema("""
type Query{
Expand Down Expand Up @@ -1418,9 +1469,48 @@ class QueryTraverserTest extends Specification {

}

def "can select an arbitrary root node with coerced variables"() {
given:
def schema = TestUtil.schema("""
type Query{
foo: Foo
}
type Foo {
subFoo: SubFoo
}
type SubFoo {
id: String
}
""")
def visitor = mockQueryVisitor()
def query = createQuery("""
{foo { subFoo {id}} }
""")
def subFooAsRoot = query.children[0].children[0].children[0].children[0].children[0]
assert subFooAsRoot instanceof Field
((Field) subFooAsRoot).name == "subFoo"
def rootParentType = schema.getType("Foo")
QueryTraverser queryTraversal = QueryTraverser.newQueryTraverser()
.schema(schema)
.root(subFooAsRoot)
.rootParentType(rootParentType)
.coercedVariables(CoercedVariables.emptyVariables())
.fragmentsByName(emptyMap())
.build()
when:
queryTraversal.visitPreOrder(visitor)

then:
1 * visitor.visitField({ QueryVisitorFieldEnvironmentImpl it ->
it.field.name == "subFoo" && it.fieldDefinition.type.name == "SubFoo"
})
then:
1 * visitor.visitField({ QueryVisitorFieldEnvironmentImpl it -> it.field.name == "id" && it.fieldDefinition.type.name == "String" && it.parentType.name == "SubFoo" })

}

@Unroll
def "builder doesn't allow ambiguous arguments"() {
def "builder doesn't allow null arguments"() {
when:
QueryTraverser.newQueryTraverser()
.document(document)
Expand All @@ -1431,7 +1521,7 @@ class QueryTraverserTest extends Specification {
.build()

then:
thrown(IllegalStateException)
thrown(AssertException)

where:
document | operationName | root | rootParentType | fragmentsByName
Expand All @@ -1446,11 +1536,24 @@ class QueryTraverserTest extends Specification {
null | "foo" | null | Mock(GraphQLObjectType) | emptyMap()
null | "foo" | null | Mock(GraphQLObjectType) | null
null | "foo" | null | null | emptyMap()
}

@Unroll
def "builder doesn't allow ambiguous arguments"() {
when:
QueryTraverser.newQueryTraverser()
.document(createQuery("{foo}"))
.operationName("foo")
.root(Field.newField().build())
.rootParentType(Mock(GraphQLObjectType))
.fragmentsByName(emptyMap())
.build()

then:
thrown(IllegalStateException)
}

def "typename special field doens't have a fields container and throws exception"() {
def "typename special field doesn't have a fields container and throws exception"() {
given:
def schema = TestUtil.schema("""
type Query{
Expand Down Expand Up @@ -1585,7 +1688,7 @@ class QueryTraverserTest extends Specification {

}

def "test accumulate is returned"() {
def "test accumulate is returned"() {
given:
def schema = TestUtil.schema("""
type Query{
Expand Down Expand Up @@ -1711,7 +1814,7 @@ class QueryTraverserTest extends Specification {

}

def "can copy with Scalar ObjectField visits"() {
def "can copy with Scalar ObjectField visits"() {
given:
def schema = TestUtil.schema('''
scalar JSON
Expand Down