-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[RFC] List coercion algorithm #1058
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for graphql-spec-draft ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like it brings list coercion to the same (currently buggy due to nullable-with-default-values) state as all the other variable coercion so this feels right to me
spec/Section 3 -- Type System.md
Outdated
- Otherwise, if {itemValue} is a Variable: | ||
- If the variable provides a runtime value: | ||
- Let {coercedItemValue} be the runtime value of the variable. | ||
- Otherwise, if the variable definition provides a default value: | ||
- Let {coercedItemValue} be this default value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a specific reason this doesn't use the "pre-coerced" variable values? (from CoerceVariableValues
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason was that this is essentially copied from the input coercion for input objects (but via an algorithm to make it clearer): https://spec.graphql.org/draft/#sec-Input-Objects.Input-Coercion
But it's a good question. I guess the reason is that coercedVariableValues
is not explicitly made available in section 3 of the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see... It may be an issue:
Ultra synthetic example:
type Query {
a(b: [[Int]]): Int
}
Operation:
query Foo($c: [Int]) {
a(b: [$c])
}
Runtime Variables:
{
"c": 42
}
If we're saying the runtime value is the "value that is sent over the wire", we end up with b = [42]
(incompatible) instead of b = [[42]]
if we coerce the variable to a list first (I think?)
If we're saying the runtime value is the "pre-coerced" value, then there's no need to mention defaultValue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That should work in GraphQL-JS as well from what I can tell
- We see that
b
expects a List https://github.com/graphql/graphql-js/blob/16.x.x/src/utilities/valueFromAST.ts#L77 - We see that
b
gets a list as an AST-Argument https://github.com/graphql/graphql-js/blob/16.x.x/src/utilities/valueFromAST.ts#L79 - We go into the for loop, the first element is a variable, we recurse https://github.com/graphql/graphql-js/blob/16.x.x/src/utilities/valueFromAST.ts#L90
- Inside of the recursion we see that this value is expected to be an array again https://github.com/graphql/graphql-js/blob/16.x.x/src/utilities/valueFromAST.ts#L77
- The received value is not a list, but a variable, we get the variable and return it in an array https://github.com/graphql/graphql-js/blob/16.x.x/src/utilities/valueFromAST.ts#L99-L103
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JoviDeCroock I think it works in graphql-js because variables
(here) are coerced already?
Because variable are coerced super early in validateExecutionArgs
?
And because CoerceVariableValues
is already taking care of variables default values, there's no need to be explicit about them here.
I think there is some value in making variableValues
available in Section 3. I opened a PR on the PR here: benjie#2
A more formal version would define a CoerceXXXValue(value, type, variableValues) for every type but that's probably out of scope for this specific PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think the last 2 steps of the code path is more like so?
- Inside of the recursion we see that this value is a variable https://github.com/graphql/graphql-js/blob/16.x.x/src/utilities/valueFromAST.ts#L49
- The variable value is returned as is and it's working fine because it was coerced to an array before: https://github.com/graphql/graphql-js/blob/16.x.x/src/utilities/valueFromAST.ts#L62
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That understanding is not what I'm saying though, notice how we return [result] rather than result? Meaning we do coerce into a list as that's the expected type of the argument, regardless of the variable type, so the variable coerced or not seems irrelevant. Or I am misunderstanding the point entirely
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On line 103 there's coercion indeed:
if (isListType(type)) {
// ....
return [coercedValue]; // line 103
}
But my understanding is that line 103 is not reached because we should enter the branch in line 49 first?:
if (valueNode.kind === Kind.VARIABLE) { // line 49
// ..
return variableValue;
}
// ...
// not reached
if (isListType(type)) {
// ...
Not super familiar with the codebase so apologies if that's absolutely not how it works but it feels like it should work like so?
76bec1c
to
6aed5a9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is also in line with GraphQL JS we deeply traverse and perform the same logic as during normal coercion on each value we find deeply
Fixes #1002.
Previously, list coercion does not detail what to do with variables at all, and that could lead to either a null pointer exception, or to double-coercion of the variable value if you're only following the spec.
Consider the following valid schema:
and the query that is valid against this schema:
NOTE: We're using the variable in a list item position!
If you issue this to the GraphQL server with variables
{"number": null}
thenCoerceVariableValues
will give you{"number": null}
and when you fast-forward toCoerceArgumentValues
you'll go in to 5.j.iii.1:https://spec.graphql.org/draft/#sel-NANTHHCJFTDFBBCAACGB0yS
coercedValues = {}
argumentValues = { numbers: [1, $number, 3] }
fieldName = 'sum'
field named {fieldName}.
argumentDefinitions = { numbers: ... }
argumentName = 'numbers'
argumentType = [Int!]!
defaultValue = undefined
{argumentName}.
hasValue = true
{argumentName}.
argumentValue = [1, $number, 3]
NOPE
{variableName}.
{variableName}.
value = [1, $number, 3]
NOT TRIGGERED
{defaultValue}.
not {true} or {value} is {null}, raise a field error.
NOT TRIGGERED
Yes, it is
It is not, it is a list
{null}.
It is not, it is a list
{value}.
YES
{argumentType}, raise a field error. TIME TO VISIT LIST COERCION
input coercion rules of {argumentType}.
{coercedValue}.
Time to visit list coercion
We need to coerce the value
[1, $number, 3]
to the non-nullable type[Int!]!
.Step 1: handle the non-null. It's not null. Great!
Now we need to coerce the value
[1, $number, 3]
to the list type[Int!]
.Here's what the spec says about input coercion for lists:
We have a list, so we only care about the bold line.
This line seems to miss a bunch of situations.
For example: if we were coercing to
[Int]
the value[1, $number, 3]
with variables{}
then is $number (which is undefined, since it wasn't provided in the variables) "accepted by the list's item type"? Really we must coerce this tonull
, but that doesn't seem to be detailed. In fact this entire section doesn't mention variables at all.We're actually coercing to
[Int!]
, so the question is: is$number
accepted by the list's item type?$number
itself is a variable, so...I've attempted to solve this problem by being much more explicit about the input coercion for lists, inspired by the input coercion for input objects. I've also added a non-normative note highlighting the risk of a null variable being fed through into a non-nullable position, why that can occur (validation) and what we do about it (field error). I've also expanded the table with both variables and many more examples to cover many more edge cases.