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

Raw and Coerced Variables refactor #2802

Merged
merged 17 commits into from May 24, 2022

Conversation

dondonz
Copy link
Member

@dondonz dondonz commented Apr 18, 2022

Why refactor? Identifying double coercion

Variables were previously represented as a Map<String, Object>, which made it difficult to determine if variables were raw or had been already coerced.

Example: double coercion in FieldValidationInstrumentation

Let's start from this line in Execution#executeOperation. At this point, variables have already been coerced. The ExecutionContext can only contain coerced variables.

InstrumentationContext<ExecutionResult> executeOperationCtx = instrumentation.beginExecuteOperation(instrumentationParams);

Then inside FieldValidationInstrumentation#beginExecuteOperation we have

List<GraphQLError> errors = FieldValidationSupport.validateFieldsAndArguments(fieldValidation, parameters.getExecutionContext());

Within validateFieldsAndArguments, we create a QueryTraverser, which coerces variables a second time.

Example: double coercion in WIP PR to shift max query depth after validation

https://github.com/graphql-java/graphql-java/pull/2773/files#diff-2696e88b9e4d095eea4d0e47b1c98ea872bfce3642c5aaee7317b1c3c2c478a5R149

This is a WIP PR so I don't want to be harsh, nevertheless it's a good illustration of how confusing raw and coerced variables are. The ExecutionContext can only contain coerced variables. The linked QueryTraverser will coerce variables a second time.

This PR adds RawVariables and CoercedVariables

This PR adds RawVariables and CoercedVariables to make the distinction clearer and avoid accidental double coercion.

This PR addresses #2764, and sets the foundation for #2773.

This is a large PR! To make it easier to see the changes, please read commit by commit (or alternatively, I can split this work into multiple PRs).

What did change

I refactored variables to RawVariables or CoercedVariables in crucial classes related to Execution and classes required for the upcoming traverser changes in #2773.

What didn't change

There are other classes containing a map of variables such as FieldCollectionParameters. These classes have been left unchanged primarily to keep the scope of this PR manageable. In these unchanged classes, there was far less ambiguity about coercion. I can extend RawVariables and CoercedVariables to all other classes in another PR.

What is in the next PR

After this PR is merged in I can help finish off #2773 and fix both double coercion examples I outlined above.

@@ -39,7 +40,7 @@ private ExecutionInput(Builder builder) {
this.context = builder.context;
this.graphQLContext = assertNotNull(builder.graphQLContext);
this.root = builder.root;
this.variables = builder.variables;
this.rawVariables = builder.rawVariables;
Copy link
Member Author

Choose a reason for hiding this comment

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

Variables in ExecutionInput are always raw

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.variables = coerceVariables(assertNotNull(variables, () -> "variables can't be null"), variableDefinitions);
this.coercedVariables = coerceVariables(assertNotNull(variables, () -> "variables can't be null"), variableDefinitions);
Copy link
Member Author

Choose a reason for hiding this comment

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

For now, keep functionality similar to before

In the next PR (#2773), QueryTraverser will change to accept already coerced variables

*/
@Internal
public class CoercedVariables {
private final ImmutableMapWithNullValues<String, Object> coercedVariables;
Copy link
Member Author

Choose a reason for hiding this comment

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

Using this special map as variable values can be null

@@ -43,7 +42,7 @@ public class ExecutionContext {
private final ImmutableMap<String, FragmentDefinition> fragmentsByName;
private final OperationDefinition operationDefinition;
private final Document document;
private final ImmutableMapWithNullValues<String, Object> variables;
private final CoercedVariables coercedVariables;
Copy link
Member Author

Choose a reason for hiding this comment

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

ExecutionContext can only contain coerced variables. This change makes it clearer

*/
@Internal
public class RawVariables {
private final ImmutableMapWithNullValues<String, Object> rawVariables;
Copy link
Member Author

Choose a reason for hiding this comment

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

Using this special map as values can be null


def argument = Argument.newArgument("if", new BooleanValue(true)).build()
def directives = [Directive.newDirective().name("skip").arguments([argument]).build()]

conditionalNodes.valuesResolver.getArgumentValues(Directives.SkipDirective.getArguments(), [argument], variables) >> ["if": 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.

This mock wasn't necessary

then:
resolvedValues['variable'] == outputValue
resolvedValues.get('variable') == outputValue
Copy link
Member Author

Choose a reason for hiding this comment

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

Most API changes in this test file

@@ -39,7 +39,7 @@ public class ExecutionContextBuilder {
Object root;
Document document;
OperationDefinition operationDefinition;
ImmutableMapWithNullValues<String, Object> variables = ImmutableMapWithNullValues.emptyMap();
CoercedVariables coercedVariables = new CoercedVariables(Collections.emptyMap());
Copy link
Member

Choose a reason for hiding this comment

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

how about CoercedVariables.emptyVariables() - reads nicer - not essential

this.coercedVariables = ImmutableMapWithNullValues.copyOf(coercedVariables);
}

public Map<String, Object> getMap() {
Copy link
Member

Choose a reason for hiding this comment

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

map name it toMap that is you are converting it from a X -> Y - from a CoercedVariables to a map representtion

executionInput.locale == Locale.GERMAN
executionInput.extensions == [some: "map"]
executionInput.query == "new query"
}
Copy link
Member

Choose a reason for hiding this comment

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

What happens if we set both

.rawVariables(x).variables(y)

Should we have a test for this. Do they go into the same place?

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'll add a test.

Yes, in ExecutionInput both rawVariables and variables set the same field. If both get set, the last one would be used (in your example y).

Is it worthwhile preventing both .rawVariables(x).variables(y) to be used? I don't mind adding another check if it'll be clearer

Copy link
Member

@bbakerman bbakerman left a comment

Choose a reason for hiding this comment

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

I generally appove strongly of this

See my specific comments on naming.

Copy link
Member

@bbakerman bbakerman left a comment

Choose a reason for hiding this comment

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

As we discussed in person - lets remove the RawVariables fron ExecutionInput signature but use it on the inside

# Conflicts:
#	src/main/java/graphql/ExecutionInput.java
#	src/main/java/graphql/execution/ValuesResolver.java
#	src/main/java/graphql/normalized/ExecutableNormalizedOperationFactory.java
Copy link
Member

@bbakerman bbakerman left a comment

Choose a reason for hiding this comment

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

Merge ! Merge ! Merge!!

@bbakerman bbakerman merged commit 86d5443 into graphql-java:master May 24, 2022
@bbakerman bbakerman added this to the 19.0 milestone May 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants