Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypePolicy on a request level #6949

Closed
tomprats opened this issue Sep 1, 2020 · 14 comments
Closed

TypePolicy on a request level #6949

tomprats opened this issue Sep 1, 2020 · 14 comments

Comments

@tomprats
Copy link

tomprats commented Sep 1, 2020

With the deprecation of updateQuery on a fetchMore, there doesn't seem to be a way to specify its replacement, a TypePolicy, in the same place. I understand that you can specify it on specific entities as seen in the docs and below:

const cache = new InMemoryCache({
  typePolicies: {
    Person: {
      fields: {
...

But in my use-case, I have a helper method used by various entities with the list query passed in:

import {useQuery} from "@apollo/client";
import {useEffect, useState} from "react";
import {nameFromQuery} from "lib/graphql/helpers";

export default function useFetchAll(query) {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const {data, fetchMore} = useQuery(query, {
    onError: setError,
    variables: {first: 100}
  });

  useEffect(() => {
    if(!data) { return; }

    const name = nameFromQuery(query);
    const {endCursor, hasNextPage} = data[name].pageInfo;

    if(hasNextPage) {
      fetchMore({
        updateQuery: (oldData, {fetchMoreResult: newData}) => ({
          ...newData,
          [name]: {
            ...newData[name],
            edges: [
              ...oldData[name].edges,
              ...newData[name].edges
            ]
          }
        }),
        variables: {after: endCursor}
      });
    } else {
      setLoading(false);
    }
  }, [data]);

  return {data, error, loading};
}

I'm not sure how I can convert this method to using type policies instead. There are a couple of ways to avoid this issue:

  • Type Policies for everything that uses this - but it would make the method unreliable
  • Skipping the cache - but it'd then it'd making a lot of extra calls if used multiple times

It'd make more sense to me if useQuery or fetchMore accepted a typePolicy, similar to how it accepts a fetchPolicy. That may be a naive statement, but there just seems like there should be an equivalent way of handling the issue if updateQuery is going away.

@vigie
Copy link

vigie commented Sep 1, 2020

I have the exact same set-up/use case.

I wish to avoid adding identical merge type policies for every kind of type that uses my pagination helper. I was giving this some thought and came up with the following idea:

All of my types that can be paginated via my helper implement a GQL Connection interface. It would be cool if we could declare a "TypePolicy" for an interface, e.g:

const cache = new InMemoryCache({
  typePolicies: {
    Connection: {
      fields: {
        connection: {
          merge(existing, incoming, { variables }) { ...}
...

@benjamn
Copy link
Member

benjamn commented Sep 9, 2020

@vigie I agree that being able to define policies for abstract interface/union types (which could then be inherited by subtypes) would be a great improvement. Do you think that would cover all of your needs, or do you have use cases where you need even more flexibility than inheritance would provide? For example, the policyForType idea I proposed in #6808 (comment).

@tomprats Does this inheritance idea make sense for your case? Happy to explain in more depth if you're not sure what this would look like.

@tomprats
Copy link
Author

tomprats commented Sep 9, 2020

@benjamn I think it definitely helps. I'd still prefer being able to do something on the request level, but this would definitely be the next best thing.

@vigie
Copy link

vigie commented Sep 18, 2020

Hi @benjamn I also have the problems described in #6808 but I think I'd rather address them in a more declarative way, allowing types (of all flavors - user defined, abstract and even custom scalar) to declare policies that include the ability to declare default merge (and other) behavior.

For example, we currently make quite heavy use of the custom JSONObject scalar (this allows for a nice shorthand allowing us to work with data coming back from a legacy REST service before we are ready to fully formalize that data into the schema).

In order to upgrade without warnings, I need to go through my entire schema and declare a field policy for all usages of this scalar to declare "replace, don't merge".

For example, for a type

type Foo {
   a: JSONObject
   b: JSONObject
}
{
   typePolicies: {
      Foo: {
         fields: {
            a: {
               merge: false
            },
            b: {
               merge: false
...

Instead, I would like to be able to attach default merge policies directly to types:

typePolicies: {
   JSONObject: {
      merge: false
   }
}

This idea also corrects a mistake I make in my pagination use case I sketched above. There, it's not that I want to attach fieldPolicies to an abstract type (although that will definitely be useful in other scenarios and should be allowed), it's that again, I want to attach a default merge policy directly to the type:

typePolicies: {
   Connection: {
      merge: myCustomMergeFunction
   }
}

As I'm sure you know, the interaction choices between a merge policy on a type and a field are:

  • Always run the type policy first, passing the result to the field policy, or
  • Field policy overwrites type policy, or
  • Field policy can optionally invoke type policy (and then possibly do something else with that result)

I'm not sure which would be most appropriate, interested to hear your thoughts.

If we had this ability then, at least as of today, I don't think I would also need the dynamic policyForType

benjamn added a commit that referenced this issue Sep 23, 2020
Motivating discussion:
#6949 (comment)

JavaScript developers will be familiar with the idea of inheritance from
the `extends` clause of `class` declarations, or possibly from dealing
with prototype chains created by `Object.create`.

In a more general sense, inheritance is a powerful code-sharing tool, and
it works well for Apollo Client for several reasons:

  1. InMemoryCache already knows about the supertype-subtype relationships
     (interfaces and unions) in your schema, thanks to possibleTypes, so
     no additional configuration is necessary to provide that information.

  2. Inheritance allows a supertype to provide default configuration
     values to all its subtypes, including keyFields and individual field
     policies, which can be selectively overridden by subtypes that want
     something different.

  3. A single subtype can have multiple supertypes in a GraphQL schema,
     which is difficult to model using the single inheritance model of
     classes or prototypes. In other words, supporting multiple
     inheritance in JavaScript requires building a system something like
     this one, rather than just reusing built-in language features.

  4. Developers can add their own client-only supertypes to the
     possibleTypes map, as a way of reusing behavior across types, even if
     their schema knows nothing about those made-up supertypes.

Inheritance is a step back (in terms of freedom/power) from the completely
dynamic policyForType and policyForField functions I proposed in
#6808 (comment),
but I think inheritable typePolicies will go along way towards addressing
the concerns raised in #6808.
@benjamn
Copy link
Member

benjamn commented Sep 23, 2020

@vigie @tomprats See #7065 for the inheritance implementation.

@vigie I have another PR in the works that I think does basically what you're asking, by allowing you to mark any type as mergeable: true, as an alternative to finding all the fields where that type might be used and marking them as merge: true.

@vigie
Copy link

vigie commented Sep 24, 2020

@vigie I have another PR in the works that I think does basically what you're asking, by allowing you to mark any type as mergeable: true, as an alternative to finding all the fields where that type might be used and marking them as merge: true.

Thanks for working on this. I would specifically need the ability to set mergeable to false and also to a custom function - will that also be handled in your pending PR?

@benjamn
Copy link
Member

benjamn commented Sep 24, 2020

@vigie Please take a look at #7070!

@vigie
Copy link

vigie commented Sep 28, 2020

Thanks @benjamn I can confirm that #7070 is working well for my Connection use case as described above in version 3.3.0-beta.5 👍

@pontusab
Copy link

I don't really know if this is about solving pagination when you have nested connections that you want to paginate? Here is an example, let's say this is my schema:

type Project {
     postsConnection(after: String): PostConnection
}

the data structure will then be data.project.postsConnection.edges etc

in an ideal world I would like to specify:

typePolicies: {
      Query: {
        fields: {
           project: {
              posts: relayStylePagination(),
           }
        },
      },
},

and call fetchMore({ after: 'next-cursor' })

How can I solve this?

@michel
Copy link

michel commented Dec 30, 2020

@pontusab , I have exactly the same usecase your defining with relayBased nested connections.

query projectMedia($projectId: ID!, $type: MediaType, $after: String) {
  project(projectId: $projectId) {
    id


    media(type: $type, after: $after, first: 30) @connection(key: "media") {
      pageInfo {
        hasNextPage
        endCursor
      }

      nodes {
        ...MediaFields

        user {
          ...UserFields
        }
      }
    }
  }
}

Just as you , i had hoped to specify the typePolicy as followed:

      Query: {
        fields: {
           project: {
              media: relayStylePagination(),
           }
        },
      },
}

But that's unfortunately not possible at the moment. How did you end up solving this?

@pontusab
Copy link

@michel Yeah I solved my issue by:

typePolicies: {
  Project: {
          fields: {
            postsConnection: relayStylePagination(),
          },
        },
 },

@heyrict
Copy link

heyrict commented Jan 3, 2021

I have a similar issue on migrating from v2 to v3 on cache updating. I used the same query while with different view types in my chat application.

Here is the sample query

query Chatmessages(...) {
  chatmessages(filter: $filter, limit: $limit, offset: $offset, order: $order) {
    ...Chatmessage
  }
}

The main part displays all messages in an infinite-scrolling method, where the following policy works great.

typePolicies: {
  Query: {
    fields: {
      chatmessages: offsetLimitPagination({keyArgs: ['filter', 'order']}),
    },
  },
},

While the other part displays logs of messages in a pages, where only $limit number of records are shown at a time. I have to use the following policy to make this part work.

typePolicies: {
  Query: {
    fields: {
      chatmessages: { keyArgs: ['filter', 'order', 'limit', 'offset'] },
    },
  },
},

As updateQuery is deprecated, is there a workaround for making both work now in v3?

@hwillson
Copy link
Member

hwillson commented May 7, 2021

The original issue here was solved by #7070. Please open subsequent issues separately. Thanks!

@hwillson hwillson closed this as completed May 7, 2021
@tomprats
Copy link
Author

tomprats commented May 7, 2021

@vigie

All of my types that can be paginated via my helper implement a GQL Connection interface

I haven't worked with graphql a lot so I'm a bit out of my element. In my project, all the list queries are specific types like FilmsCollection or PlanetsConnection seen in this example.

In comparison are yours of type Connection? So it looks like I'd still have to specify the type policy on each of these individual connections. Just wanted to double check if I'm missing something

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants