-
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: Define custom scalars in terms of built-in scalars. #914
Conversation
If there's general agreement, I'll be following up with more test coverage as well as the minor spec text change to cover the introspection addition. |
528f8fb
to
0a2aec0
Compare
Can I add another wish list item here? I don't know if it's specifically relevant to this PR. It would be really nice to be able to override the serialization and deserialization of built-in types in GraphQL-JS. Specifically, I always run into this with |
I think that's out of scope here but happy to discuss in another issue or pr |
@@ -41,6 +41,8 @@ union AnnotatedUnionTwo @onUnion = | A | B | |||
|
|||
scalar CustomScalar | |||
|
|||
scalar StringEncodedCustomScalar = String |
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.
bikeshedding:
= doesn't seem like the appropriate operator here because they're not equal, but one is a subset of the other
maybe a different operator like :
or a keyword like as
or of
?
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.
Yeah, I agree. implements
seems like the closest thing, :
seems reasonable 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.
@dschafer also suggested :
IIRC. My preference for =
was based on matching the rest of the SDL forms, but I agree that the implication of equality isn't correct.
I'm a bit worried that :
is also overloaded - we most use it for defining the types or values to arguments but also aliases.
Maybe a keyword is best to make this as literate as possible, we rejected :
in favor of implements
for the same reason when defining object types. I kind of like as
as short for "serializes as". Thoughts from others?
85a0203
to
c9b713f
Compare
@leebyron — An issue that seems directly relevant to this RFC is validation and scalar compatibility. Sometimes we'll want to transition a scalar field from "String" to something more descriptive, like "ID" or custom "DateTime" scalar. For example: type Query {
foos(ids: [String!]): [Foo]
} to: type Query {
foos(ids: [ID!]): [Foo]
} Unfortunately, client queries which refer to that field as # This old query would fail to validate after the otherwise-benign schema change,
# and there is no solution currently short of updating all clients in lockstep,
# or adding a new argument name.
query($myFooIds: [String!]) {
foos(ids: $myFooIds) { bar }
} Would this PR help with that kind of transition? For example, would it make sense to allow input fields whose scalars serialize to compatible JSON to emit warnings on type mismatches, rather than a validation error? |
Will this change also help with "asymmetric" scalars? For example, in our system we've a Token type which is (roughly) |
c9b713f
to
637ee10
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.
Some thoughts after coming back to this
src/type/definition.js
Outdated
@@ -478,12 +480,14 @@ export class GraphQLScalarType { | |||
// Serializes an internal value to include in a response. | |||
serialize(value: mixed): mixed { | |||
const serializer = this._scalarConfig.serialize; | |||
return serializer(value); | |||
const serialized = serializer(value); | |||
return this.ofType ? this.ofType.serialize(serialized) : serialized; |
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 effectively chains the serialize functions together if more than one are provided. Should we consider having the same chaining behavior for parsing input values 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.
Agreed 👍
context: SchemaValidationContext, | ||
scalarType: GraphQLScalarType, | ||
): void { | ||
if (scalarType.ofType && !isSpecifiedScalarType(scalarType.ofType)) { |
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 wonder if this is over-constrained. Perhaps it would be okay to allow a scalar to serialize in terms of any other scalar type?
My thought is that people add custom scalars for two reasons. First is to represent some higher level concept like "Email" or "PhoneNumber" where "serializes as String" is useful. Second is to represent a new kind of transport primitive like "BigInt" where they definitionally serialize as something new. However that first kind of higher-level concept scalar might want to say it serializes in terms of another non-spec scalar!
In other words - what makes the specification of a scalar special to warrant this limitation?
I believe we originally settled on this limitation to avoid needing to follow a chain of "ofType" to determine the final actual serialization type, but I think that might not be such a big deal
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 believe we originally settled on this limitation to avoid needing to follow a chain of "ofType" to determine the final actual serialization type, but I think that might not be such a big deal
I think that decision about allowing recursive types should be done in the scope of the entire spec since it affects the entire type system. For example, you couldn't query all information about a type using __type
anymore:
{
__type(name: "LocalPath") {
name
# ...
ofType {
name
# ...
# <= you can't construct a query that gets arbitrary long chain of `ofType`
}
}
}
Or the fact that you can introduce loops inside types, so we need to make buildASTSchema
, buildClientSchema
and others loop-resistant. E.g.:
scalar A as C
scalar B as A
scalar C as B
I think allowing to better describe scalars isn't worth added complexity. However, if the similar changes are done in other places of type-system (e.g. graphql/graphql-spec#295) then the price is already paid and we could definitely add recursiveness to scalar types.
However that first kind of higher-level concept scalar might want to say it serializes in terms of another non-spec scalar!
We can allow specifying scalar serializable as any other scalar without as
in the definition.
scalar BigInt
scalar Counter as BigInt # Valid
scalar UserCounter as Counter # Invalid
But with current terminology and SDL syntax, it would be very confusing 😞
That's why I think we need to separate custom scalar type
s and scalar type
s:
custom scalar BigInt
scalar Counter as BigInt # Valid
scalar UserCounter as Counter # Invalid
So we could say that scalar is serializable only as a standard scalar or a custom scalar.
I think this PR in its current form solves >80% of use-cases so we can release it as is and relax rules afterward.
@@ -448,6 +448,7 @@ export type ScalarTypeDefinitionNode = { | |||
+loc?: Location, | |||
+description?: StringValueNode, | |||
+name: NameNode, | |||
+type?: NamedTypeNode, |
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 this key should be more descriptive. A type having a field called "type" is confusing
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.
Fully agree. How about serializableAs
?
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.
@leebyron Given current syntax scalar Url as String
.
How about asType
?
@asaf-romano - that would still work fine - the internal representation as an object would first go through your custom scalar's serialize function before going through the serializing type's. That guarantees the result of the query will be a string, but still gives control over the internal representation |
Additional tests should be added to ensure validation works as expected |
@leebyron I rebased this PR on top of |
This proposes an additive change which allows custom scalars to be defined in terms of the built-in scalars. The motivation is for client-side code generators to understand how to map between the GraphQL type system and a native type system. As an example, a `URL` custom type may be defined in terms of the built-in scalar `String`. It could define additional serialization and parsing logic, however client tools can know to treat `URL` values as `String`. Presently, we do this by defining these mappings manually on the client, which is difficult to scale, or by giving up and making no assumptions of how the custom types serialize. Another real use case of giving client tools this information is GraphiQL: this change will allow GraphiQL to show more useful errors when a literal of an incorrect kind is provided to a custom scalar. Currently GraphiQL simply accepts all values. To accomplish this, this proposes adding the following: * A new property when defining `GraphQLScalarType` (`ofType`) which asserts that only built-in scalar types are provided. * A second type coercion to guarantee to a client that the serialized values match the `ofType`. * Delegating the `parseLiteral` and `parseValue` functions to those in `ofType` (this enables downstream validation / GraphiQL features) * Exposing `ofType` in the introspection system, and consuming that introspection in `buildClientSchema`. * Adding optional syntax to the SDL, and consuming that in `buildASTSchema` and `extendSchema` as well as in `schemaPrinter`. * Adding a case to `findBreakingChanges` which looks for a scalar's ofType changing.
637ee10
to
a0cf9b2
Compare
@leebyron I rebased this PR on top of |
This proposes an additive change which allows custom scalars to be defined in terms of the built-in scalars. The motivation is for client-side code generators to understand how to map between the GraphQL type system and a native type system. As an example, a
URL
custom type may be defined in terms of the built-in scalarString
. It could define additional serialization and parsing logic, however client tools can know to treatURL
values asString
. Presently, we do this by defining these mappings manually on the client, which is difficult to scale, or by giving up and making no assumptions of how the custom types serialize.Another real use case of giving client tools this information is GraphiQL: this change will allow GraphiQL to show more useful errors when a literal of an incorrect kind is provided to a custom scalar. Currently GraphiQL simply accepts all values.
To accomplish this, this proposes adding the following:
A new property when defining
GraphQLScalarType
(ofType
) which asserts that only built-in scalar types are provided.A second type coercion to guarantee to a client that the serialized values match the
ofType
.Delegating the
parseLiteral
andparseValue
functions to those inofType
(this enables downstream validation / GraphiQL features)Exposing
ofType
in the introspection system, and consuming that introspection inbuildClientSchema
.Adding optional syntax to the SDL, and consuming that in
buildASTSchema
andextendSchema
as well as inschemaPrinter
.Adding a case to
findBreakingChanges
which looks for a scalar's ofType changing.