-
Notifications
You must be signed in to change notification settings - Fork 2k
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: Default value validation & coercion #3049
Conversation
@@ -98,7 +98,6 @@ const TestType = new GraphQLObjectType({ | |||
}), | |||
fieldWithNestedInputObject: fieldWithInputArg({ | |||
type: TestNestedInputObject, | |||
defaultValue: 'Hello World', |
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 actually caused a bunch of failures in the existing suite, so that's promising!
src/type/validate.js
Outdated
if (isInputObjectType(fieldType)) { | ||
const isNonNullField = | ||
isNonNullType(field.type) && field.type.ofType === fieldType; | ||
if (isNonNullField || !isEmptyValue(field.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.
This really just adds a condition, implementing this comment suggestion
I definitely need to write tests to ensure this is behaving as expected
src/utilities/coerceInputValue.js
Outdated
// Default values are initially represented as internal values, "serialize" | ||
// converts the internal value to an external value, and "parseValue" converts | ||
// back to an internal value. | ||
return type.parseValue(type.serialize(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.
A key example of this is Enums, where the external (string) and internal (any) representations differ. Custom scalars can have the same property
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 don't this this is correct:
serialize
is for coercing the values of resolved values (the return values from Resolver). It takes the result of a resolver and produces an result value ("result coercion" is the more correct term here).
parseValue
is for coercing input runtime values (programming language specific values). The two use cases for that kind of coercing are variables values and default values. A GraphQLInputField
contains a defaultValue
which needs to be coerced via parseValue
.
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 would be a change in behaviour for graphql.js, but I think that change is desired - specifying that the default for an enum is "MY_CUSTOM_SORTER"
rather than require('sorters').myCustomSorter
(which may happen to be a function) makes sense to me - it's more in line with specifying the value via JSON within a variable.
I think Lee's solution may work to maintain the current behaviour - if you return an enum value (which may happen to be a function) from a resolver then that gets converted via serialize
; similarly if you have the exact same enum value (i.e. a function) within defaultValue
then using serialize to turn it back into the GraphQL enum makes sense to me.
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.
Let me expand a bit, why I think this can't work in general.
The possible output values of serialize
can be completely disjunct from the input values of parseValue
. They simply have an orthogonal purpose. parseValue
will represent an "internal input value" and should accept a wide range of different inputs and return a good representation of the value to be received as input for Resolver.
The output of serialize
on the other hand is the final result of a field.
In practice most Scalars probably accepts serialize
values as input for parseValue
, but this is not required at all.
I could define a Currency
scalar for example which has a special CurrencyOutput
as the result of serialize
which is not accepted as input for parseValue
.
Also there is nothing that actually forces you to implement to serialize
in the first place. See https://github.com/jaydenseric/graphql-upload/blob/master/public/GraphQLUpload.js for an example.
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.
Thanks for clarifying Andi; that makes sense.
From a quick scan this looks good - the memoization is straightforward and leverages the flexibility of JS. I was expecting to do this with a schema WeakMap with a defaults map inside it, but your way is massively more efficient. It might be worth adding an initial Can we move the When it comes to validation I'm going to have to dedicate a longer block of time to reviewing that. |
Smart
Definitely, good suggestion #3050 |
153e8b9
to
8ed299d
Compare
One interesting aspect here is that a default value declared via SDL is coerced twice: I am not really sure this is a problem, but I wanted to call that out. |
Thanks @andimarek - #3051 |
src/type/validate.js
Outdated
// which can throw to indicate failure. If it throws, maintain a reference | ||
// to the original error. | ||
try { | ||
parseResult = type.serialize(inputValue); |
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.
Similar to the other comment: serialize
is the result coercion. Scalar and Enums check the validity of input values via parseValue
.
Heads up: this diff is about to expand in scope. After spending some time on this today - I'm realizing that #3051 and related comments are actually quite significant and difficult to untangle from this change. The fact that This issue is made most apparent by Enum values, which have a different representation externally (a string value or Enum literal) and internally (any value). Some significant changes are necessary to represent |
@leebyron we have actually a similar challenge in graphql java: the IIRC one of the big roadblocks is the problem mentioned in #3051: if you |
Updates in this last commit:
I don't think this is near complete yet, but wanted to get the work in progress up. |
26fb0fc
to
d3e4125
Compare
src/type/definition.js
Outdated
try { | ||
// For leaf types (Scalars, Enums), serialize is the oppose of coercion | ||
// (parseValue) and will produce an "external" value. | ||
return namedType.serialize(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.
as outlined in a previous comment I think this is wrong: we are misusing serialize
here to produce a representation for an input value. We should introduce a specific function inputValueToAst
for the Coercion and leave it to the scalar itself to determine a printable representation of its input values.
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.
We talked about this, but writing it down here for posterity:
I agree this is a misuse. This "uncoerce" step should ideally not exist, and only does so to avoid introducing a breaking change to existing users.
The previous code path used astFromValue(defaultValue)
- astFromValue
misuses serialize
in the same way. This carries over the misuse to preserve existing behavior but isolates it to this specific case.
The new code path is valueToLiteral(uncoerceDefaultValue(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.
I am not sure this will always preserve backwards compatibility.
Some scalars throw on serialize as a way of preventing them from being erroneously used as output values. While the example I know of should never have a default value, other examples might. See jaydenseric/graphql-upload#194
I would suggest that as part of the definitive solution that you appear on the cusp of delivering:
(A) the spec and the implementation should provide a mechanism of labeling leaf values as only valid within either output or input values
Separately, I would suggest:
(B) the implementation should introduce programmatic internalDefaultValue/externalDefaultValue options for arguments where only one may be defined, with a deprecation notice for the use of the previously ambiguous defaultValue. This would preserve the current behavior as best as possible, but also provide a clear upgrade path.
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 am not sure this will always preserve backwards compatibility.
Some scalars throw on serialize as a way of preventing them from being erroneously used as output values.
Since the previous codepath used valueToAST
which also called serialize()
on these, I believe such a scalar would already exhibit this behavior, so no change. This just moves the call to serialize()
.
(A) the spec and the implementation should provide a mechanism of labeling leaf values as only valid within either output or input values
This is an interesting idea! It's out of scope for this RFC, but I see the use case for giving GraphQL validation this information, rather than waiting to fail at execution time.
(B) the implementation should introduce programmatic internalDefaultValue/externalDefaultValue options for arguments where only one may be defined, with a deprecation notice for the use of the previously ambiguous defaultValue. This would preserve the current behavior as best as possible, but also provide a clear upgrade path.
I agree with this! I'd like for this "uncoerce" method to have a reasonably short shelf life and to be able to be removed in a future major version, but I'm looking for a solution that allows the end state to look correct. IMHO having to provide externalDefaultValue
for the foreseeable future would be too confusing.
I'm not opposed to introducing a more modest breaking change, but would look to @IvanGoncharov for guidance on when and how to introduce that. Some ideas:
-
Require
defaultValue
to validate as an external input value, but provide a newdeprecatedCoercedDefaultValue
field which uses this new method for preserving behavior.Any problematic internal defaultValue would need to manually edit their schema to either fix the value or use this new field. The schema validation error message could reference this field so fixing an existing schema is self-documenting.
-
Provide a "flag" like "allowDeprecatedCoercedDefaultValues" when constructing a schema which applies the behavior currently in this PR. That would be a much easier change to make (one place, instead of per-call-site) but doesn't help highlight the long-term necessary changes and unnecessarily would run "uncoerce" on already uncoerced values.
I'll note that most defaultValue tend to be simple built-in scalars, so it's very likely that the vast majority will upgrade to a version including this change and not need to do anything at all. defaultValue for Enum or some Custom Scalars will need to be addressed.
I bet @andimarek has thoughts on this as well
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 think option 1 is best and imho that it is very important to fix the reference implementation to reflect the spec properly.
Thanks for your additional comments above, much appreciated.
Let me say, that I am really happy that we tackle this issue. Great work @leebyron @benjie In order to understand the other comments I made more clearly I will try to paint a more complete picture of what I believe is missing and we should do. Currently we have three different coercion methods:
Default values for arguments or Input object fields can be created in two different ways: via SDL or programmatically directly. A value specified via SDL can be coerced via The missing part here becomes obvious when we look at Introspection default values or in SDL printing.
But we currently have no way of actually producing a Literal value from an "internal input value". This means there is actually no way of printing a custom Scalar default value correctly. The solution I see is making the Scalar responsible for that by adding another coercion method: lets call it The relationship between
Regarding the double coercion problem mentioned above: I think we must clearly define what Looking for some feedback, especially if we agree on the fundamental aspects of what I described here. Thanks! |
I’m with you in principle but I think we can take a much simpler approach
if we can represent defaultValue as an “external input value”, much like
variables. We already have all the necessary well-defined methods for
dealing with these in AST and introspection.
We should only need to define how external input values become coerced and
how internal output values become coerced. These are the only two defined
by the spec.
The primary problem is that we have a bunch of existing uses of graphql.JS
which provide defaultValue as an internal input value instead of an
external input value. We need a graphql.JS specific (outside the bounds of
the spec) way to reverse input coercion specifically and only for this case.
|
I agree that defining I am happy that you say agree in principal, but I think our mental models around Coercion are not completely the same. For example I think that the spec should be more clear about the different kinds of coercions and coercion is "underspecified" in the spec. There is no one rule how to coerce input values, because we have two very different input coercion ways: from AST values which should be clearly defined, but also from programmatic values, which is very technology dependent. The combination of JS, JSON serialization and JS like GraphQL syntax makes it very easy to look at think they are the same, but I think it is very important to distinguish it clearly. As you said yourself:
But in order to get more clarity let me give a specific example Scalar to make the discussion more concrete: I define a
Now if I define a SDL: type Query {
salary(employee: EmployeeReference = "MTIzNA=="): String
} The current PR would then go on and invoke What is the current idea how we deal with that? |
I want to share what GraphQL Java is planning to do about this issue in our code base, because fundamentally we have exactly the same challenge as graphql.js. We have But we want to always coerce default values going forward, but also don't break existing usages. This is why we will make default value coercing optional: if you set This means As explained before for the It may not look so nice as solution first, but this dual state of |
That mostly matches what I'm planning here - I just want to make sure we're clear on terminology because I think
This exactly matches what I'm proposing here.
This seems wrong if I think I need a flow chart to illustrate all the value states and the functions for moving between them. That might help guide the discussion |
Another thing to add here - seems like we agree that our goal is to find a way to move to a better method of representing
My primary problem with this is that existing services will have to define something new in order to continue to function. My goal is to introduce this without a breaking change or a change that requires additional work to adopt. Requiring scalars to implement this new function would hinder this goal |
Maybe we can split this into two tasks. Introducing
Yes absolutely:
That is exactly what The flow for this expression is (from inside out)
I agree that requiring all Scalars suddenly to implement this is not a good approach. I am not suggesting to break everybody, but give Scalars at least the possibility to produce correct Ast representations. |
EDIT: This graphic is no longer totally accurate: #3074 (in the stack referenced at the original comment) implements "New Introspection Path for SDL defined Schema" - it's non breaking and fixes a related but different bug (#3051). The "Input Value Un-coercion" has been changed to only be used to create validation error messages, not produce values which will be used at runtime.
|
Some things to note in those flow diagrams above:
|
Thanks a lot for the picture @leebyron . In order to get more clarity, let me just focus on one specific aspect which I don't fully agree with: The box I don't think both things can be true at the same time: What do you think? |
I totally agree. This is currently broken, or at least poorly named. There's still a lot of work left to clean up this PR, but the next change coming will essentially rename "valueFromAST" to "coerceInputLiteral". GraphQL.js defineds "valueFromASTUntyped" which is a better fit for the "AST to Value" step in the picture |
55e3e32
to
1259418
Compare
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed. GraphQL behavior should not change, though error messages are now slightly different.
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed. GraphQL behavior should not change, though error messages are now slightly different.
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed. GraphQL behavior should not change, though error messages are now slightly different.
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either -- but not both -- "value" and "literal" fields. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
Fixes graphql#3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either -- but not both -- "value" and "literal" fields. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in graphql#3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ```
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
[#3074 rebased on main](#3074). Depends on #3809 @leebyron comments from original PR (edited, hopefully correctly): > Fixes #3051 > > This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either (EDIT: but not both!) "value" and "literal" fields. > > Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: > > **Before this change:** > > ``` > (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) > ``` > > `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in #3049. > > **After this change:** > > ``` > (SDL) --parse-> (defaultValue literal config) --print --> (SDL) > ``` Co-authored-by: Lee Byron <lee.byron@robinhood.com>
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
…itionRule` (#4194) ### TLDR => let's consider moving validation of variable positions with respect to one of into the `VariablesInAllowedPositionRule`, i.e. out of the `ValuesOfCorrectTypeRule`. <hr> This work was pulled out of the work rebasing the Default Value Coercion/Validation PR stack at #3814 on the OneOf and Experimental Fragment Variables changes. In that PR stack (work originally done by @leebyron within #3049), Lee extracts the validation done by the `ValuesOfCorrectTypeRule` into a `validateInputLiteral()` utility that can be called at validation-time or at run-time. At validation time, `validateInputLiteral()` validates statically, is not supplied variables **or their types**, and does not validate variables, while at run-time it is supplied variables and does validate them. <hr> With OneOf Input Objects, we have a situation in which Input Objects will define technically nullable/optional input fields, but the addition of the `@oneOf` directive tells the validator/executor that we have to supply exactly one of these fields (and it cannot be given the value `null`). In essence, we are saying that when `@oneOf` is applied, the fields of the input object are no longer technically nullable positions, although they are still optional. This was for a combination of type reasoning, backwards compatibility, and simplicity, working overall within the constraints of GraphQL where only nullable fields are optional. So, to validate variable usage with OneOf Input Object, we need to make sure that a variable with a nullable type is not allowed to show up in a field position on a OneOf Input Object. Where should this be done? Currently, this is done within the `ValuesOfCorrectTypeRule`, which for this purpose was modified to collect the operation variable definitions. @leebyron 's changes in the abovementioned stack extracts this to `validateInputLiteral()`, where we run into a problem, we don't have access to the operation (or experimental fragment variables)! Lee's work preserving variable sources and definitions organizes the definitions by source, so if we are analyzing statically, we don't have the source or the definitions. There are two paths forwards. One idea is to modify Lee's work, and split the definitions from the sources, and supply them to `validateInputLiteral()` even when working statically, which complicates the signature of a number of functions. What if we take a step back, and ask ourselves if we should have really modified `ValuesOfCorrectTypeRule` to collect all those operation variable definitions? If we move this bit to `VariablesInAllowedPositionRule`, then we avoid the entire complexity. That's what this PR does. <hr> How did this happen anyway? Shouldn't it be clear from the spec change in which validation rule the changes should have gone? Actually....not exactly. According to [the proposed spec changes](graphql/graphql-spec#825), this work was not done within the `ValuesOfCorrectTypeRule` or the `VariablesInAllowedPositionRule`, but instead within a wholly new "OneOf Input Object Rule." That's not the way it got implemented, and in my opinion for good reason! I'm planning on separately submit some comments on the RFC to that effect, that we can eliminate the need for the new rule, and fold the changes into the existing `ValuesOfCorrectTypeRule` -- which basically says that if it can't coerce, it's not valid, and because of the coercion changes does not require any actual new text -- except within the `VariablesInAllowedPositionRule`. Anyway, TLDR TLDR => let's consider moving validation of variable positions with respect to one of into the `VariablesInAllowedPositionRule`, i.e. out of the `ValuesOfCorrectTypeRule`. Discussion of allowed variable positions just belongs within that rule, just look at the rule name. :)
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
Factors out input validation to reusable functions: * Introduces `validateInputLiteral` by extracting this behavior from `ValuesOfCorrectTypeRule`. * Introduces `validateInputValue` by extracting this behavior from `coerceInputValue` * Simplifies `coerceInputValue` to return early on validation error * Unifies error reporting between `validateInputValue` and `validateInputLiteral`, causing some error message strings to change, but error data (eg locations) are preserved. These two parallel functions will be used to validate default values in graphql#3049 Potentially breaking if you rely on the existing behavior of `coerceInputValue` to call a callback function, as the call signature has changed, or to throw with the default callback function. Grossly similar behavior is available with `validateInputValue()`, but with a separate function. GraphQL behavior should not change, though error messages are now slightly different.
Closed in favor of #3814 |
[#3049 rebased on main](#3049). This is the last rebased PR from the original PR stack concluding with #3049. * Rebased: #3809 [Original: #3092] * Rebased: #3810 [Original: #3074] * Rebased: #3811 [Original: #3077] * Rebased: #3812 [Original: #3065] * Rebased: #3813 [Original: #3086] * Rebased: #3814 (this PR) [Original: #3049] Update: #3044 and #3145 have been separated from this stack. Changes from original PR: 1. `astFromValue()` is deprecated instead of being removed. @leebyron comments from #3049, the original PR: > Implements [graphql/graphql-spec#793](graphql/graphql-spec#793) > > * BREAKING: Changes default values from being represented as an assumed-coerced "internal input value" to a pre-coerced "external input value" (See chart below). > This allows programmatically provided default values to be represented in the same domain as values sent to the service via variable values, and allows it to have well defined methods for both transforming into a printed GraphQL literal string for introspection / schema printing (via `valueToLiteral()`) or coercing into an "internal input value" for use at runtime (via `coerceInputValue()`) > To support this change in value type, this PR adds two related behavioral changes: > > * Adds coercion of default values from external to internal at runtime (within `coerceInputValue()`) > * Removes `astFromValue()`, replacing it with `valueToLiteral()` for use in introspection and schema printing. `astFromValue()` performed unsafe "uncoercion" to convert an "Internal input value" directly to a "GraphQL Literal AST", where `valueToLiteral()` performs a well defined transform from "External input value" to "GraphQL Literal AST". > * Adds validation of default values during schema validation. > Since assumed-coerced "internal" values may not pass "external" validation (for example, Enum values), an additional validation error has been included which attempts to detect this case and report a strategy for resolution. > > Here's a broad overview of the intended end state: > > ![GraphQL Value Flow](https://user-images.githubusercontent.com/50130/118379946-51ac5300-b593-11eb-839f-c483ecfbc875.png) --------- Co-authored-by: Lee Byron <lee@leebyron.com>
Depends on #3086
Implements graphql/graphql-spec#793
BREAKING: Changes default values from being represented as an assumed-coerced "internal input value" to a pre-coerced "external input value" (See chart below).
This allows programmatically provided default values to be represented in the same domain as values sent to the service via variable values, and allows it to have well defined methods for both transforming into a printed GraphQL literal string for introspection / schema printing (via
valueToLiteral()
) or coercing into an "internal input value" for use at runtime (viacoerceInputValue()
)To support this change in value type, this PR adds two related behavioral changes:
coerceInputValue()
)astFromValue()
, replacing it withvalueToLiteral()
for use in introspection and schema printing.astFromValue()
performed unsafe "uncoercion" to convert an "Internal input value" directly to a "GraphQL Literal AST", wherevalueToLiteral()
performs a well defined transform from "External input value" to "GraphQL Literal AST".Adds validation of default values during schema validation.
Since assumed-coerced "internal" values may not pass "external" validation (for example, Enum values), an additional validation error has been included which attempts to detect this case and report a strategy for resolution.
This is the final PR in a stack which builds up to supporting this RFC behavior change:
Here's a broad overview of the intended end state: