With the new Swift CodeGen engine, I'd like to see distinct types for each object's GraphQL id.
One of the great strength's is Swift's type system and it's near C++ level of zero-level abstraction.
If we were to change the id: GraphQLID of each GraphQL schema object into its own struct that has a single property (GraphQLID) it would make passing around data much more purposeful without leaving room for mistakes:
func fetchPerson(by id: PersonID)
func fetchBooking(by id: BookingID)
as it stands today, you could pass any arbitrary String to either of those functions, even if semantically the contents of the strings are wildly different. (It's invalid to pass a BookingID to any mutation/query that works with People)
John Sundell talks about this as type-safe identifiers, and PointFree has a framework that provides some inspiration: swift-tagged.
I've done something similar with Redis keys: RedisKey
Is this something already implemented or planned? I didn't see any issues regarding it, nor in the original RFC.
Is this something that is _not_ acceptable? If so, why?
Would it make sense to turn GraphQLID into a protocol at that point?
And then each of the strongly-typed IDs can conform to it, as well as String itself:
protocol GraphQLID {
public var identifier: String { get }
}
extension String: GraphQLID {
public var identifier: String { self }
}
(I'd definitely want some bikeshedding happening, I'm not sold on the API above)
Definitely not currently planned.
Two huge places I see this being a problem are with interface types and union types. If we used this in our Star Wars Schema, would we want to be generating CharacterID or HumanID and DroidID? If you pass CharacterID(2001), should that look for a Human or a Droid? What if identifiers are unique per individual type but not per interface type? This also winds up exploding the number of types and amount of code we'd need to generate when there are huge numbers of types per interface, and several of our larger clients have well over 100 types conforming to a single interface.
I definitely get the theory, but I do feel like the theory runs up a bit against what's possible given the GraphQL spec.
Isn鈥檛 that somewhat solved by having GraphQLID being a protocol? Although that drops the purpose of being strongly typed in those cases because now ANY GraphQLID can be passed
and for union types, could an enum rather than a struct be a solution?
enum InterfaceID: GraphQLID {
case human(HumanID)
case droid(DroidID)
var identifier: String {
// ... switch and use case's ID
}
}
though your point still stands about the explosion of types which is already quite large.
Although that drops the purpose of being strongly typed in those cases because now ANY GraphQLID can be passed
yep. At that point we've done a whole lot of work to get back to where String was.
So the enum thing - this is something that bit me as I was working through codegen for union and interface types. There may be members of a union or interface that are impossible values for a particular instance because of various compounded type things. So you can't always just say well, we know this is a character, becuase it could be a character that conforms to a certain interface or interfaces. At that point, you lose the benefit of having an enum, because there's then cases in the enum which aren't actually possible. Especially when every enum or or union type has dozens of members.
I've pinged @martijnwalraven, who's got terrifyingly deep knowledge of the more bizarre edge cases in the spec, I'd also like to hear what he's got to say on this.
I've used a similar tagged Identifier pattern myself in another project, so I definitely understand the benefits of type-safe identifiers. And I think it would be great if we could find a way to make something similar work with GraphQL.
I don't think focusing on id and GraphQLID is a workable solution though. GraphQL does not prescribe the use of id, or even how GraphQLID is meant to be used (whether it is globally unique or only unique within a type for example).
We've seen many different approaches to identity in the wild, especially when dealing with schemas that combine data from various domains and backends. That's why with Apollo Federation we've introduced a @key directive that is used to annotate a type with one or more combinations of fields that can be used as an identity.
For example, this would indicate that products can be uniquely identified by their upc:
type Product @key(fields: "upc") {
upc: UPC!
name: String
}
One interesting addition to this model is that @keys can also be defined on interfaces. If Product had been an interface for example, that means any object type implementing that interface would guarantee it can be uniquely identified by upc (so making upc unique across all implementing types).
There's much more to say about this, but I've long wanted to reuse the @key directive for cache configuration. And that may also give us an opportunity to realize your idea about type-safe identifiers.
Just throwing out the first idea that comes to mind, but maybe we could generate a Product.Key type that can act as a reference to any product, both for local use with direct cache reads/writes and for querying the server. So you would have references like Product.Key(upc: "123456789").
One complication is that with Apollo Federation we actually support multiple keys as a way to indicate that there are different ways of uniquely identifying a type.
This would indicate that you can uniquely identifying products either by their upc or their sku for example:
type Product @key(fields: "upc") @key(fields: "sku") {
upc: UPC!
sku: SKU!
name: String
}
The ability to specify multiple keys has turned out to be especially important for federated schemas, where you may be combining data from different systems or even different organizations. And it's also helpful with interfaces, because it allows you to 'retrofit' existing types so they can be identified by a common key. For example, you could have an existing Book type that used isbn as its key, but then implement Product and expose upc as an alternative key.
Not sure yet if this is workable, but perhaps that means we should generate keys like Product.Key as enums and expose constructors so you could do either Product.Key(upc: "123456789") or Product.Key(sku: "abc67") (anonymous enum cases aren't possible unfortunately, so we'd have to somehow generate unique case names).
We should bring @benjamn in on this since I know there's some extent to which the IDs are being used in AC3 for Web, even though they aren't necessarily prescribed by the spec.
As far as I know, Apollo Client 3 for web introduces a keyFields configuration option, which isn't limited to id but allows you to specify one or more fields that acts as keys, similar to the @key directive.
One thing we also need to think about here is the Identifiable protocol - we definitely want to adopt that for SwiftUI reasons, and there's a bit of an open question as to whether we should be doing something like keyFields for that or whether we should only be auto-generating it for things with a GraphQLID. I'm not entirely sure how that fits in with this proposal, beyond "If we have too many ways to identify something it's going to get very confusing"
@designatednerd I think these two concerns are definitely related and for my purposes could potentially be solved together.
The root problem is that we would prefer compiler assistance in preventing sending an ID that represents Bookings in a mutation for Customer, as it stands with both accepting a String, it ignores the domain semantics of what that value actually represents.
How that is actually done is what's left to explore
Right, that makes sense, although for some graphs that's total overkill since GraphQLIDs are generated as UUIDs unique not just to the type but across the graph. But I do think the "unique to a type, not across types" situation is way more common.
I think that the @key directive is going to be "key" 馃槑 to making this work. There is a lot of thought that has to go into what this will look like. I'm not for or against implementing something along these lines currently, but until we have got the new Codegen more stabilized, anything along these lines probably isn't too feasible. Any detailed discussion of this until we are closer to a position where we could actually implement it feels like conjecture.
That's not to say it's not valuable to consider the idea at a high-level. I like this concept, and taking the future possibility into account while we explore the foundations of the new Codegen can be valuable. So thank you for bringing this up @Mordil!
If you type your GraphQL id's, isn't this already possible? Make the id of a user a custom scalar type UserID, then do the normal Apollo decoding to make it into a Tagged type?
type User {
id: UserID!
}
type Photo {
id: PhotoID!
}
It's certainly possible if you use custom scalars for each individual ID type, yes.
I think @Mordil is talking about doing this as a wrapper around default GraphQLIDs that the rest of your graph would not already be required to use, though.
Yes, but also that several scalars are just being generated as String rather than a distinct type of its own that wraps a string.
For example:
{
"kind": "SCALAR",
"name": "NaiveDateTime",
"description": "The `Naive DateTime` scalar type represents a naive date and time without\ntimezone. The DateTime appears in a JSON response as an ISO8601 formatted\nstring.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
is always translated as a String property, rather than a unique type of NaiveDateTime
Is this a codegen or schema issue?
@Mordil Are you using passthroughCustomScalars?
Ah, looks like we aren't. We're using the default value of .none in the codegen settings.
I'll try that out!