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

Add valueToLiteral() #3065

Closed
wants to merge 1 commit into from
Closed

Conversation

leebyron
Copy link
Contributor

@leebyron leebyron commented May 6, 2021

Depends on #3077

Provides the "Value to Literal" methods in this data flow chart.

  • Adds valueToLiteral() which takes an external input value and translates it to a literal, allowing for custom scalars to define this behavior.

This also adds important changes to Input Coercion, especially for custom scalars:

  • The value provided to parseLiteral is now ConstValueNode and the second variables argument has been removed. For all built-in scalars this has no effect, but any custom scalars which use complex literals no longer need to do variable reconciliation manually (in fact most do not -- this has been an easy subtle bug to miss).

    This behavior is possible with the addition of replaceVariables

@leebyron
Copy link
Contributor Author

leebyron commented May 6, 2021

@andimarek this is my attempt at introducing the version of "toLiteral" we discussed the other day.

@leebyron
Copy link
Contributor Author

leebyron commented May 6, 2021

Open for discussion for a future change is whether replaceASTVariables should even exist. I'm not thrilled with it, but it's necessary to be a non-breaking change.

We currently allow variables to exist in the middle of a complex custom scalar:

query ($var: String) {
  example(arg: { field: $var })
}

type Query {
  example(arg: CustomScalar): String
}

scalar CustomScalar

This currently passes validation "All Variable Usages are Allowed" because if we do not know the expect type of a variable's position then we allow it - and the position in the example above is contained within a scalar - it does not have an expected type.

The validation rule in the spec does not specify this behavior, it just assumes a variable position has a type which based on this example is not necessarily a safe assumption.

We should probably make this behavior clear. Either by allowing it in the spec (and support this existing behavior) or disallow it in the spec making the above example invalid (which would allow us to assert that anything passed to parseLiteral is already ConstValue and therefore replaceASTVariables could be removed)

@andimarek
Copy link
Contributor

Open for discussion for a future change is whether replaceASTVariables should even exist. I'm not thrilled with it, but it's necessary to be a non-breaking change.

We currently allow variables to exist in the middle of a complex custom scalar:

query ($var: String) {

  example(arg: { field: $var })

}



type Query {

  example(arg: CustomScalar): String

}



scalar CustomScalar

This currently passes validation "All Variable Usages are Allowed" because if we do not know the expect type of a variable's position then we allow it - and the position in the example above is contained within a scalar - it does not have an expected type.

The validation rule in the spec does not specify this behavior, it just assumes a variable position has a type which based on this example is not necessarily a safe assumption.

We should probably make this behavior clear. Either by allowing it in the spec (and support this existing behavior) or disallow it in the spec making the above example invalid (which would allow us to assert that anything passed to parseLiteral is already ConstValue and therefore replaceASTVariables could be removed)

The most popular example is the JSON Scalar which can contain variables in its literals. I don't really think we can break that behavior.

@leebyron
Copy link
Contributor Author

leebyron commented May 6, 2021

That's the main use case I'm aware of, but I'm curious how common it is - it seems weird to allow one well-defined scalar to be embedded within another well-defined scalar. There's almost certainly going to be some weird edge-case behavior in there. From a usefulness point of view - I see the merit. From a clarity of execution and option-value preservation point of view, it seems wise to restrict complex scalars to be always fully formed rather than provided in pieces

We should probably clarify that in the spec either way.

@andimarek
Copy link
Contributor

andimarek commented May 6, 2021

I want to point out a challenge regarding validation here:

This query:

query($var: ID){hello(arg: {a: $var})}

should be invalid for this schema:

  scalar JSON

  type Query {
    hello(arg: JSON): ID
  }

at least according to the implementation of parseLiteral in the custom scalar (https://github.com/taion/graphql-type-json/blob/master/src/index.js#L45) because ID is not an allowed Literal for a JSON value.

But because it is passed in as variable the custom Scalar just accepts the coerced variable.

This comes fundamentally from the fact that the type system treats Scalars as leaf types with no further inside. But variables inside custom Scalars break this assumption.

Note: this was tested against the current version. This PR would probably fixed that problem as far as I can see. So that would be great @leebyron

@leebyron
Copy link
Contributor Author

leebyron commented May 6, 2021

Yeah you've got this exactly right. What does it mean to have a variable within a leaf value? There is no way to represent that in the type system.

*/
export function replaceASTVariables(
valueNode: ValueNode,
variables: Maybe<ObjMap<unknown>>,
Copy link
Contributor

@andimarek andimarek May 6, 2021

Choose a reason for hiding this comment

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

just to be clear here: these are the raw (uncoerced variables) right?

I recommend to name it like that rawVariables or uncoercedVariables

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are coerced variables, which is also the existing behavior. We have no other type system to apply within a custom scalar, so the types of the variables are used for their coercion before they're folded in.

Copy link
Contributor

@andimarek andimarek May 7, 2021

Choose a reason for hiding this comment

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

We are calling valueToLiteral here on the variables which means from my understanding it must be raw variables, as valueToLiteral is external input value (aka raw variables) to Literal.

Edit: I just saw that the type argument is missing to valueToLiteral which is the cause of my confusion: replaceAstVariables needs to have a type argument which then needs to be passed on valuesToLiteral, because otherwise it is all undefined behavior again.

@andimarek
Copy link
Contributor

One important detail:

What should the formal relationship between valueToLiteral and literalToValue be?

For example for ID: This code does valueToLiteral(literalToValue(AstInt(123))) == AstInt(123)
but also valueToLiteral(literalToValue(AstString("123"))) == AstInt(123)

This means valueToLiteral maps two different values to the same Literal (In math speak: it is not injective) and both functions are not the inverse to each other.

If we want to make both functions the inverse to each other we would require that different literals always produce different external input values and different external input values produce different literals. I don't think we can do that as this is a constraint we have not had so far and we would break existing code.

This probably means we require that for every valid literal x literalToValue(valueToLiteral(literalToValue(x))) is possible and for every valid input value y valueToLiteral(literalToValue(valueToLiteral(y))) is possible, but not more.

@andimarek
Copy link
Contributor

I want to state that I think getting rid of variable references inside complex ast literal is the right way to preserve the current behaviour while making custom scalar implementations easier and more robust. Great work @leebyron

*
* Note: This function does not perform any type validation or coercion.
*/
export function valueToLiteral(
Copy link
Contributor

@andimarek andimarek May 7, 2021

Choose a reason for hiding this comment

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

I think this method tries to be too clever: the type argument should never be undefined and the whole conversion logic should be based on the type.

If it is a List type => take the value apart and perform it per value.
If Object type => iterate over field definitions and call it per field
If Scalar/Enum => call the valueToLiteral on its type definition.

If you have an input object as type and the value is a boolean it should raise an error: at the moment it returns a boolean literal which is wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about this, but the problem with this approach is that we're operating on "external" pre-coerced, pre-validated values and it's entirely possible that those values do not match the types. These conversion functions are used before validation takes place in #3049. That may require us to define validation rules again in each of these functions, ideally they can be passed through so that we define validation once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We also need reasonable defaults for when we don't have a type, since that's going to be common for any custom scalars with complex definitions (like the JSON type)

Copy link
Contributor

Choose a reason for hiding this comment

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

Took me a bit to wrap my head around, but I think our misunderstanding comes from a fundamental principles about Scalar.literalToValue/Scalar.valueToLiteral does: I think these two methods should follow the same principles as parseValue/parseLiteral and should throw errors if something is wrong. I want basically another "coercing" (already a bit overloaded term btw) which converts from Value and Literal and vice verse.

Looking at the ID implementation in this PR I see that exactly this is missing: currently it returns undefined instead of throwing GraphQLError. Building on this principle I think method is basically the equivalent of coerceInputValueImpl but going from value to literal .

The reason I want this: for example for a JSON literalToValue method : you would want to throw a clear error message for an expected Literal deep inside the provided literal. What is the alternative? Return undefined? Silently ignore?

There is also general value in having a well defined, validated literal or external value for inputs.

And as you mentioned: this would mean validated twice, yes. But I don't see logic defined twice really: all validation logic resided inside the Scalar except for Input Objects. And I am sure we can extract/generalize this logic if that is a concern.

To be specific: we check for two things when validating input objects: if all required fields are provided and not more fields are provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't necessarily disagree with that. I think being more strict is useful, and I'm sure we could find a way to generalize the validation portion.

The root of the issue is still variables within custom scalars. We have no types there and all bets are off. As long as we decide to preserve that behavior we need untyped transform

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think I follow completely regarding the variables:

We actually have a type of the variables and we could use that type to convert the External input value into an Ast Literal in a well defined manner. We could then simply replace the VariableReference ast node with the Ast Literal we got.

Custom Scalar implementation can't type check variables values anyway at the moment: they just get a variable and they basically take the value.

I think that would preserver the current behavior.

@andimarek
Copy link
Contributor

Looking at your flow chat: I fully agree regarding the new well defined behavior defined by valueToLiteral and literalToValue. 👍

I commented inline, but want to highlight it here again: I think we are trying to conserve some not well defined behavior which we don't have to: valueFromAst (and the revsere) should be deprecated and left alone imho. It should never be used anymore inside GraphQL.js execution itself as fas as I can tell.

@leebyron
Copy link
Contributor Author

leebyron commented May 7, 2021

valueFromAst is misnamed, as it does "uncoercion" and needs to stay in use until we can fix default values in #3049 - it's deprecated in that PR

Base automatically changed from const-value-ast to main May 8, 2021 06:22
@leebyron leebyron marked this pull request as draft May 11, 2021 01:26
@leebyron leebyron changed the base branch from main to input-value-config May 11, 2021 05:08
@leebyron leebyron changed the base branch from input-value-config to default-value-literals May 11, 2021 05:08
leebyron added a commit that referenced this pull request May 11, 2021
Depends on #3074

By way of introducing type `VariableValues`, allows `getVariableValues` to return both the coerced values as well as the original sources, which are then made available in `ExecutionContext`.

While variable sources are not used directly here, they're used directly in #3065. This PR is pulled out as a pre-req to aid review
leebyron added a commit that referenced this pull request May 11, 2021
Depends on #3074

By way of introducing type `VariableValues`, allows `getVariableValues` to return both the coerced values as well as the original sources, which are then made available in `ExecutionContext`.

While variable sources are not used directly here, they're used directly in #3065. This PR is pulled out as a pre-req to aid review
leebyron added a commit that referenced this pull request May 11, 2021
Depends on #3074

By way of introducing type `VariableValues`, allows `getVariableValues` to return both the coerced values as well as the original sources, which are then made available in `ExecutionContext`.

While variable sources are not used directly here, they're used directly in #3065. This PR is pulled out as a pre-req to aid review
leebyron added a commit that referenced this pull request May 15, 2021
Depends on #3074

By way of introducing type `VariableValues`, allows `getVariableValues` to return both the coerced values as well as the original sources, which are then made available in `ExecutionContext`.

While variable sources are not used directly here, they're used directly in #3065. This PR is pulled out as a pre-req to aid review
leebyron added a commit that referenced this pull request May 15, 2021
Depends on #3065

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 #3049
leebyron added a commit that referenced this pull request May 15, 2021
Depends on #3074

By way of introducing type `VariableValues`, allows `getVariableValues` to return both the coerced values as well as the original sources, which are then made available in `ExecutionContext`.

While variable sources are not used directly here, they're used directly in #3065. This PR is pulled out as a pre-req to aid review
leebyron added a commit that referenced this pull request May 15, 2021
Depends on #3065

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 #3049
leebyron added a commit that referenced this pull request May 15, 2021
Depends on #3065

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 #3049
@leebyron leebyron added the PR: feature 🚀 requires increase of "minor" version number label Jun 1, 2021
leebyron added a commit that referenced this pull request Jun 1, 2021
Depends on #3065

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 #3049
* Adds `valueToLiteral()` which takes an external value and translates it to a literal, allowing for custom scalars to define this behavior.

This also adds important changes to Input Coercion, especially for custom scalars:

* The value provided to `parseLiteral` is now `ConstValueNode` and the second `variables` argument has been removed. For all built-in scalars this has no effect, but any custom scalars which use complex literals no longer need to do variable reconciliation manually (in fact most do not -- this has been an easy subtle bug to miss).

  This behavior is possible with the addition of `replaceASTVariables`
yaacovCR added a commit that referenced this pull request Sep 29, 2024
[#3077 rebased on
main](#3077).

Depends on #3810

@leebyron comments from original PR:

> By way of introducing type `VariableValues`, allows
`getVariableValues` to return both the coerced values as well as the
original sources, which are then made available in `ExecutionContext`.
> 
> While variable sources are not used directly here, they're used
directly in #3065. This PR is pulled out as a pre-req to aid review

---------

Co-authored-by: Lee Byron <lee.byron@robinhood.com>
yaacovCR added a commit that referenced this pull request Sep 29, 2024
[#3065 rebased on
main](#3065).

Depends on #3811

@leebyron comments from original PR:

> **Provides the "Value to Literal" methods in this [data flow
chart](https://user-images.githubusercontent.com/50130/118379946-51ac5300-b593-11eb-839f-c483ecfbc875.png).**
> 
> * Adds `valueToLiteral()` which takes an external input value and
translates it to a literal, allowing for custom scalars to define this
behavior.
> 
> **This also adds important changes to Input Coercion, especially for
custom scalars:**
> 
> * The value provided to `parseLiteral` is now `ConstValueNode` and the
second `variables` argument has been removed. For all built-in scalars
this has no effect, but any custom scalars which use complex literals no
longer need to do variable reconciliation manually (in fact most do not
-- this has been an easy subtle bug to miss).
>   This behavior is possible with the addition of `replaceVariables`

Changes to the original:
1. Instead of changing the signature of `parseLiteral()`, a new method
`parseConstLiteral()` has been added with the simpler signature.
`parseLiteral()` has been marked for deprecation.
2. `replaceVariables()` has access to operation and fragment variables.

Co-authored-by: Lee Byron <lee.byron@robinhood.com>
@yaacovCR
Copy link
Contributor

Merged as #3812

@yaacovCR yaacovCR closed this Sep 29, 2024
yaacovCR added a commit that referenced this pull request Oct 27, 2024
[#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: feature 🚀 requires increase of "minor" version number
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants