If your schema implements Global Object Identification, your graphQL queries return serialized IDs. This could be something difficult to manage, in particular if you need to stitch different schemas that implement Relay specifications.
We have this (semplified) schemas:
schema "books
"
type Query {
node(id: ID!): Node
nodes(ids: [ID!]!): [Node]!
books(first: Int after: String last: Int before: String order: [BookSortInput!] where: BookFilterInput): BooksConnection
bookById(id: String!): Book
}
type BooksConnection {
pageInfo: PageInfo!
edges: [BooksEdge!]
nodes: [Book!]
totalCount: Int!
}
type Book implements Node {
id: ID!
authors: [Author!]!
publisher: Publisher
relatedBooks: [Book!]!
title: String
abstract: String
editionVersion: Int
publicationDate: DateTime
categories: [String!]
availability: [Inventory!]!
}
schema "inventories
"
type Query {
inventoryById(id: String!): Inventory
}
type Inventory implements Node {
id: ID!
productId: String!
productType: ProductType!
inStock: Boolean!
availableQty: Int!
plant: String
}
In the stiched schema we want to do this kind of query:
{
books(first: 2) {
nodes {
id
title
availability {
productId
inStock
availableQty
}
}
}
}
The stich to do is something like the following:
extend type Book implements Node {
availability: [Inventory!]! @delegate(path: "inventoriesByProductId(productId: $fields:id)")
}
But here we could have a problem because the Relay query for book return serialized book ids that CANNOT be resolved by the inventoriesByProductId
unless it is implemented with Global Object Identification in mind with the ID
attribute like this:
public async Task<Inventory[]> GetInventoriesByProductId(
[ID] string productId,
InventoryByProductDataLoader dataLoader)
=> await dataLoader.LoadAsync(productId);
But, again, here we'll have a side-effect problem because now this inventoriesByProductId
query can accept only GOI (serialized) ids, which is not what we probably want.
There's a little package that includes an interceptor for Hotchocolate that do what we want: hotchocolate-polymorphic-ids.
It allows all the arguments/input marked as ID
to be passed both as serialized or "original" id.
dotnet add ./graphqlWarehouse package AutoGuru.HotChocolate.PolymorphicIds --version 2.0.0
and do this service setup:
builder.Services
.AddPolymorphicIds()
...
Sometimes it's convinient to add an extra field in your typer for all of your IDs resolved with Relay specification. In that way you can choose if to use serialized or original ID. You can do it with this helper:
(see: https://gist.github.com/benmccallum/89d4d5b604d67094418956db43386ce5)
public static IObjectFieldDescriptor ImplementsNodeWithDbIdField<TNode, TId>(
this IObjectTypeDescriptor<TNode> descriptor,
Expression<Func<TNode, TId>> idProperty,
NodeResolverDelegate<TNode, TId> nodeResolver)
where TNode : class
{
// Add dbId which should just return the internal id as is
var idPropertyFunc = idProperty.Compile();
var dbIdFieldDescriptor = descriptor
.DbIdField()
.Resolve(ctx => idPropertyFunc(ctx.Parent<TNode>()));
// This is a bit dodgy but not sure how else to force it.
// Some seem to wanna be String not String!
if (typeof(TId) == typeof(string))
{
dbIdFieldDescriptor.Type<NonNullType<StringType>>();
}
// Call the standard HC setup methods
return descriptor
.ImplementsNode()
.IdField(idProperty)
.ResolveNode(nodeResolver);
}
public static IObjectFieldDescriptor DbIdField<T>(this IObjectTypeDescriptor<T> descriptor)
=> descriptor.Field(DbId);
public static IObjectFieldDescriptor DbIdField(this IObjectTypeDescriptor descriptor)
=> descriptor.Field(DbId);
public static IInterfaceFieldDescriptor DbIdField<T>(this IInterfaceTypeDescriptor<T> descriptor)
=> descriptor.Field(DbId);
public static IInterfaceFieldDescriptor DbIdField(this IInterfaceTypeDescriptor descriptor)
=> descriptor.Field(DbId);
To use in this way (replacing the Node implementation):
public class InventoryType : ObjectType<Inventory>
{
protected override void Configure(IObjectTypeDescriptor<Inventory> descriptor)
{
descriptor
.ImplementsNodeWithDbIdField(
idProperty: f => f.Id,
nodeResolver: (ctx, id) =>
ctx.DataLoader<InventoryBatchDataLoader>().LoadAsync(id, ctx.RequestAborted));
}
}