Skip to content

Latest commit

 

History

History
167 lines (137 loc) · 4.69 KB

15.polymorphic-ids.md

File metadata and controls

167 lines (137 loc) · 4.69 KB

Polymorphic IDs

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.

The problem

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.

The solution

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));
	}
}