Trying to understanding how type policies are a substitute for `updateQuery`

I’m trying to understand how type policies could possibily replace all instances of updateQuery or cache writes in general. Current documentation and deprecation warnings are all pushing us to migrate our fetchMore updateQuery cache writes to type policies merge functions, but from what I can tell for even fairly mundane use cases type policies cannot possibly have enough context in order to know how to update the cache.

I’m hoping someone can explain how I am mistaken.

Two objections I have to the current deprecation warnings, 1) a ease of use concern about type policies, 2) a substantial objection:

1) Type policies are significantly more complicated to configure and more error prone than cache writes using writeQuery:

  • They demand an understanding of cache behaviour around normalisation and querying the cache using sucessive readField function calls. This creates a very high barrier to entry for my team to be able to do anything with paginated queries.
    • writeQuery is often just a setter where you are dealing with more understandable operation types, and/or you just concating or filtering an array of your operation types.
  • In a TypeScript context you are pretty much flying blind even when using codegen tooling such as TypedTypePolicies from graphql-codgen because nearly everything within a merge or read method is any.
    • in contrast writeQuery and updateQuery have excellent static analysis support provided you use TypedDocumentNode produced from graphql-codgen

2) A type policy lacks sufficient context to be able to replace updateQuery or writeQuery

With the current interface its impossible to know what the appropriate merge implementation should be, and as far as I can tell it can never replace writeQuery or updateQuery.

Consider the following type policy below. I have a Ticket type that has a field called messages that takes mulitple args and returns a cursor based paged query result with non-normalisable edges that contain a message type that do have an id that can be normalised by the default policies.

const ticketPolicy: TypedTypePolicies = {
  Ticket: {
    fields: {
      messages: {
        keyArgs: ["order"],
      },
    },
  },

  PagedTicketMessages: {
    keyFields: false,
    merge: false,
    fields: {
      edges: {
        merge(existing = [], incoming, util) {
          // impl here
        },
      },
    },
  },

  PagedTicketMessagesEdge: {
    keyFields: false,
    merge: false,
  },
};

Now if I use fetchMore to get the next page I basically want to concat the incoming result with the existing result, which would suggest this as my type policy:

  merge(existing = [], incoming, util) {
    // or the concat utility that comes with apollo 
    return [...existing, ...incoming];  
  }

However, I also have mutation operations that delete, re-order, and update the items in this list. Assume I have this delete mutation:

mutation deleteTicketMessage($messageId: ID!) {
  deleteTicketMessage(messageId: $messageId) // return Boolean
}

and I have this update mutatation cache write:

update: (cache, _, { variables }) => {
  const messages = cache.readQuery({
    query: GetTicketMessagesDocument,
    variables: { ticketId },
  });
  if (messages) {
    const edges = messages.ticket.messages.edges.filter(
      (edge) => edge.message.id !== variables.messageId
    );
    cache.writeQuery({
      query: GetTicketMessagesDocument,
      variables: { ticketId },
      data: {
        ...messages,
        ticket: {
          ...messages.ticket,
          messages: { ...messages.ticket.messages, edges: edges },
        },
      },
    });
  }
}

So now my merge function will be invoked by two different kinds of operations - a fetchMore that gets the next page where incoming will be a set of results meant for concatenation, and a delete operation where incoming will be the same as existing but with the deleted message filtered out.

From the point of view of the merge function I have essentially zero context about which one of these operations is being executed, and I have no hint from any of the arugments as to whether I should accept the incoming or concat with the existing. All I have is the arguments for the original query, but nothing else.

The same dilemma repeats itself for any other operation I might want to have on this list. In this context I have no choice but to completely ignore the deprecation warning and use updateQuery instead of a type policy. There is no reasonable way for me to implement this type policy.

Am I completely wrong about this, or has there been a complete oversight about how inadaquate the current type policy interface is for replacing updateQuery?

4 Likes