question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Have `cache.modify` functions receive `options.args`

See original GitHub issue

It would be very helpful if cache.modify received options.args, similar to what read and merge functions do.

A current case that we come across, is that we have large lists of items in our cache, which are under the same field name but queried with a status argument/variable. When creating a new item, we currently query not only the resulting new item, but the lists as well. To increase speed and reduce over-fetching, we changed the mutation to only query the newly created object itself, and manually add it to several lists using cache.modify. However, the new item should only be appended to the field values which correspond to certain arguments. To do this, we now rely on parsing of storeFieldName strings: a much preferred solution would be to directly work with the arguments, but for that they would have to be provided through options.args.

Since @benjamn extensively described some of the practical implications in a previous comment, I’ve included this:

@jedwards1211 I would like to see cache.modify functions receive options.args, somewhat like read and merge functions currently do.

For cache.modify specifically, I’m imagining options.args would be the most recent arguments object (possibly null) from the last time the field’s value was updated, rather than anything more complicated, like a history of arguments or variables.

Implementing options.args would be somewhat tricky right now (which mostly means we are unlikely to get to it before AC3 is released), because field arguments are not currently saved (or reversibly encoded) in the cache’s internal representation. However, I don’t see any immediate problems (other than slightly increasing cache size) with the EntityStore saving the original, unfiltered arguments alongside their corresponding field values somehow, so the arguments could be recovered without any parsing of storeFieldName strings.

Historically, storing the original arguments alongside field values has not been necessary for reading and writing queries or fragments, because the given query/fragment plus variables allows computing specific, unambiguous arguments for any field. What you’ve sketched so far with cache.updateFragment in #6335 seems like it follows this unambiguous path, too, leaving cache.modify and cache.evict as the only two APIs that modify the cache without the guidance of a query/fragment plus variables, potentially acting on multiple field values per field name, which might have been written using different arguments.

Both modify and evict are new in AC3, so I’d like to give them some time to bake, so we can see how people (want to) use them. That said, I’m pretty sure the options.args idea is technically feasible, without a major @apollo/client version bump.

In the meantime, please feel free to share any compelling use cases that options.args (or something similar) could solve.

_Originally posted by @benjamn in https://github.com/apollographql/apollo-client/pull/6289#issuecomment-634316625_

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:20
  • Comments:16 (9 by maintainers)

github_iconTop GitHub Comments

20reactions
raysuelzercommented, Oct 7, 2021

This is an issue for far more than pagination, it impacts invalidation and modification on ROOT_QUERIES with keyArgs. The fact that the details passed to Modifier<T> do not include variables, other than what is serialized in the storeFieldName makes modifying queries which share the same field name but have a large number of variables extremely difficult, the same is true with cache.evict. I think exposing stored keyArgs as part of details in the Modifier<T> function would be very helpful, as it is already is stored in the storeFieldName as a string. Unfortunately, to get those args into a JS object requires ugly hacks to parse the cacheKey to a javascript object.

For example, imagine a query which has 4-5 optional variables which are all used a keyargs. There may be hundred or more in the cache (think typeahead), with a huge number of potential combinations. If you add an item via a mutation, there is no easy way to search through the cache to determine which queries you need to modify because you have NO access to the keyargs. You literally would have to guess every possible combination and check if it exists in the cache.modify function.

I appologize for any errors in the code below. As I am typing it directly here.

query GetItems(
  $campaignId: ID
  $type: String  
  $keyword: String
  $targetType: String
) {
    getItems(
    campaignId: $campaignId
    type: $type
    keyword: $keyword
    targetType: $targetType
) {
        id
        name
        type
    }
}

Assume all of these variables are used as keyArgs. Every time a user updates keyword, it will create a new cache entry, or if they change the type, etc. Assume I add a new item of type “Foo” to campaign id “1”. That that means I need to figure out which GetItems queries have a campaignId of “1” or a type of foo and modify them.

For example this update function using cache.modify, doesn’t really leave me a great way to figure out which of the getItems on the root query I really care about.

            update: (cache, result) => {
                cache.modify({
                    id: 'ROOT_QUERY',
                    fields: {
                        getItems(value, details) {
                            // Returns every query, but no access to keyArgs
                            console.log(details.storeFieldName) // getFields{"type":"Bar", keyword: "f"}
                        }
                    }
                })

What would be better

            update: (cache, result) => {
                cache.modify({
                    id: 'ROOT_QUERY',
                    fields: {
                        getItems(value, details) {
                           // allow checking the keyArgs
                            if (details.keyArgs.type === result.type) {
                                 // do update on fields where the type matches the mutation result
                                 // the other args are irrelevant (keyword, campaign, etc)
                             }
                             // return other values, they don't need to be modified
                             return value
                        }
                    }
                })

This also extends to cache.evict.

I use this hacky helper function frequently, to determine which queries I need to evict. invalidateApolloCacheFor takes two arguments. The first, is the Apollo cache. The second is a function which receives the field and key args of every ROOT_QUERY item in the cache. If the filter returns true, the helper will evict from the cache.

The main “trick” here is the helper parses the cache key (which has the key args included) into an object containing the field name and key args. The filter function is then invoked with this new object. This helper could be removed if there was a way to get at the keyArgs for each field in the cache other than parsing the storeFieldName string.

            update: (cache, result) => {
               invalidateApolloCacheFor(cache, (field, keyArgs) => {
                 return field === 'getItems' && keyArgs.type === result.type
              });
}
export const invalidateApolloCacheFor = (
    cache: ApolloCache<any>,
    fieldAndArgsTest: FieldAndArgsTest) => {

    // Extracts all keys on the root query
    const rootQueryKeys = Object.keys(cache.extract().ROOT_QUERY);

    const itemsToEvict = rootQueryKeys
        .map(key => extractFieldNameAndArgs(key))
        .filter(r => fieldAndArgsTest(r));

    itemsToEvict.forEach(({ fieldName, args }) => {
        cache.evict(
            {
                id: 'ROOT_QUERY',
                fieldName,
                args
            }
        );
    });
};

export const extractFieldNameAndArgs = (key: string) => {
    if (!key.includes(':')) {
        return { fieldName: key, args: null };
    }
    const seperatorIndex = key.indexOf(':');
    const fieldName = key.slice(0, seperatorIndex);
    const args = convertKeyArgs(key);

    return { fieldName, args };
};

// Convert the keyArgs stored as a string in the query key
// to an object.
const convertKeyArgs = (key: string): Record<string, any> => {
    const seperatorIndex = key.indexOf(':');
    const keyArgs = key.slice(seperatorIndex + 1);

    // @connection directives wrap the keyArgs in ()
    // TODO: Remove when legacy @connection directives are removed
    const isLegacyArgs = keyArgs?.startsWith('(') && keyArgs.endsWith(')');

    const toParse = isLegacyArgs ? keyArgs.slice(1, keyArgs.length - 1) : keyArgs;

    // We should have a string here that can be parsed to JSON, or null
    // getSafe is an internal helper function that wraps a try catch
    const args = getSafe(() => JSON.parse(toParse), null);
    return args;
};

4reactions
hect1ccommented, Dec 4, 2022

Hey I had the same issue some time ago but implemented a solution which has helped me so far and seems to work well, so I’ll post it here if it helps anyone. Note: the one caveat is that I need to be able to get my input args wherever I use this but that hasn’t been an issue for me

I just hash the keyArgs in my type policies when I need it on specific fields

import hash from 'object-hash'

Query {
 fields: {
  usersGet: {
    keyArgs: hash,
   }
 }
}

Then in my cache modify whenever I need to use it I just rebuild the key and this allows me to get the data I need or perform operations specific to that dataset and return the structure accordingly

import hash from 'object-hash'

.....
// below is the main piece where I hash the input args which have been hashed in my type policy
// this allows me to then be able to get the exact ref I want to manipulate in my cache.modify
const hashKey = `usersGet:${hash({ input: { filter: { userIds: [userId] } } })`

cache.modify({
 fields: {
  user:  (ref) => {
   const usersGetCache = ref[hashKey]
   
   // do whatever manipulation you need
   
   return {
    ...ref,
    [hashKey]: {
     // return data 
    }
   }
  }
 }
})

We use namespaces (ie. user) but this should be enough for you to repurpose for your use cases. Hope this helps as I remember struggling on this for a lonnng time a year or so ago

Read more comments on GitHub >

github_iconTop Results From Across the Web

Reading and writing data to the cache - Apollo GraphQL Docs
Directly modifying cached fields, cache.modify, Manipulate cached data without using ... and that you have imported the gql function from @apollo/client .
Read more >
Customizing the behavior of cached fields - Client (React)
You can customize how a particular field in your Apollo Client cache is read and written. To do so, you define a field...
Read more >
Cache Updates | urql Documentation
The optimistic functions receive the same arguments as updates functions, except for parent , since we don't have any server data to work...
Read more >
Advanced Topics on Caching – Angular
Using update gives you full control over the cache, allowing you to make ... It can also take a query named argument, which...
Read more >
Django's cache framework
A final point about Memcached is that memory-based caching has a disadvantage: ... These arguments are provided as additional keys in the CACHES...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found