Description
I'm hoping to discuss solutions for the use case of upgrading field argument input type / scalars from less-strict to more strict types; consider:
# Current graph api
type Query { person(id: string): Person }
type Mutation {
addPerson(person: PersonArgs)
setPersonType(id: String, type: String)
}
# Desired eventual graph api (after deprecation lifecycle)
type Query { person(id: PersonId): Person } # compatible Scalar
type Mutation {
addPerson(person: NewPerson) # compatible InputObject
setPersonType(id: PersonId, type: PersonType) # multiple compatible Scalars
}
Where there isn't a reasonable way to make this change over time, because any change to the graph will break queries that use variables:
# This id variable triggers a validation error, even though the wire format is the same
query GetPerson($id: String) {
person(id: $id)
}
Motivation
This feels like (and certainly is for me) a very common use case where, you may start with a less specific type (frequently a String
scalar) and would like to upgrade to a well-typed value over time (e.g. a BigDecimal
, a DateTime
, an enum, etc).
Similarly, you may want to change the name of an InputObject
, even if the structure of the argument hasn't changed to be able to provide a consistent and predictable API to consumers as business needs grow and change (separately, you'd of course like to be able to deprecate fields on InputObjects).
I feel this is an especially frequent issue for rapidly prototyped graph APIs, that you'd like to harden and polish after they've already been deployed into a production environment.
The current way I'd make this change is an extraordinarily painful process both for the provider and consumers of an API:
-
Add a new field with the new arguments (or old ones; doesn't really matter) and
deprecate the previous field:type Query { person(id: String): Person @deprecated "A temporary field; will be renamed back to 'Person'" personByPersonId(id: PersonId) }
-
Wait for all consumers to switch off the old field.
-
Change the original field and de-deprecate it, deprecating the temporary field
type Query { person(id: PersonId): Person personByPersonId(id: PersonId): Person @deprecated }
-
Eventually remove the temporary, now deprecated, field.
Hopefully, an RFC that make this problem easier to solve reduces the amount of friction involved in gradual enhancement of a graph schema without being forced to choose between having a well-typed schema vs. having a predictable schema.
Ideas/Suggestions
I will add ideas from the thread here as they are discussed (Last Edited: Oct 25, 2018)
A. Overloading fields
The spec could allow fields to be overridden; the executor would choose which variant to resolve through a combination of the name and number of arguments and the type of input variables.
There would be ambiguity when NOT using variables, which could be resolved by:
- Always choosing the first overload
- Requiring that a default overload by specified by an annotation (e.g.
@defaultOverload
)
The @deprecated
attribute could be applied as normal to any of the overloads
type Query {
field person(id: String): Person @deprecated
field person(id: PersonId): Person
}
B. Anonymous Input Unions (aka. sum types) w/ deprecatable variants
This is similar to many of the InputUnion suggestions, with the key difference being in how
variables are validated. With an anonymous input union, a variable should be specified
using any of the variant names, rather than the a new InputUnion name.
type Query {
field person(id: String @deprecated | PersonId): Person
}
This suffers from all of the same variant discrimination problems as previous work on input unions:
see #395, #488,
and #202.
C. Less strict type inference of variables using ofType
We could allow scalar variables types to be valid if the wire representation of the scalar is passed as the type of the variable instead of the more strict type; relying on something like #521.
For example,
scalar PersonId(String) # NOTE: not actual syntax, but use whatever machinery is provided in PR-521.
type Query {
field person(id: PersonId): Person
}
# This now validates successfully.
query GetPerson($id: String) {
person(id: $id)
}
Unfortunately, by only relying on that change, this only converting builtin scalars to well typed scalars (but not vice-versa) and doesn't provide a solution for renaming InputObject
s.
D. Allowing explicit type coercions
We could come up with a way to annotate that type coercion between any two types is supported.
This is somewhat of an extension of #521.
@convert(from String)
scalar PersonId
input Point2D {
x: Float
y: Float
}
@convert(from Point2D)
input Point3D {
x: Float
y: Float
z: Float
}
Currently this idea is my favorite (syntax and details to be worked out).