Skip to content

Implement @fieldPolicy directive #3534

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

Open
comontes opened this issue Mar 21, 2025 · 5 comments
Open

Implement @fieldPolicy directive #3534

comontes opened this issue Mar 21, 2025 · 5 comments

Comments

@comontes
Copy link

Summary

Summary

I have a very simple use case that doesn't seem to be handled by the normalised cache properly.

I have a query that returns a list of launches:

query LaunchList {
  launches {
     launches {
      id
      site
      mission {
        name
        missionPatch(size: SMALL)
      }
    }
  }
}

I then have a second query that queries for a specific launch:

query LaunchDetails($launchId: ID!) {
  launch(id: $launchId) {
      id
      site
      mission {
        name
        missionPatch(size: SMALL)
      }
  }
}

The data returned for the first query populates the cache, but is not used for the second query.

I have set up custom cache keys in the SchemaConfiguration.swift:

import ApolloAPI

public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration {
    public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? {
        if type.typename == "Launch", let id = object["id"] as? String {
            let cacheKeyInfo = CacheKeyInfo(
                id: id,
                uniqueKeyGroup: type.typename
            )
            print("DEBUG: Generated cache key: \(cacheKeyInfo)")
            return cacheKeyInfo
        }

        return nil
    }
}

But Apollo seems to only correctly return cached data for the same query, despite the 2 pieces of data in the cache having the same cache key. This is unexpected. I get his log:

DEBUG: Generated cache key: CacheKeyInfo(id: "110", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "109", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "108", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "107", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "106", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "105", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "104", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "103", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "102", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "101", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "100", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "99", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "98", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "97", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "96", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "95", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "94", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "93", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "92", uniqueKeyGroup: Optional("Launch"))
DEBUG: Generated cache key: CacheKeyInfo(id: "91", uniqueKeyGroup: Optional("Launch"))
"LaunchListQuery Data came from the NETWORK"
DEBUG: Generated cache key: CacheKeyInfo(id: "110", uniqueKeyGroup: Optional("Launch"))
"LaunchDetailsQuery Data for launchID:110 came from the NETWORK"

I was under the impression that If I assign an id and same uniqueKeyGroup these would work?

Version

1.18.0

Steps to reproduce the behavior

Minimal test case: https://github.com/comontes/ApolloiOSBug1

Logs

Anything else?

No response

@comontes comontes added bug Generally incorrect behavior needs investigation labels Mar 21, 2025
@AnthonyMDev AnthonyMDev changed the title Normalised Cache Not Using Data from Different Query Implement @fieldPolicy directive Mar 24, 2025
@AnthonyMDev
Copy link
Contributor

This use case will be supported by the implementation of the @fieldPolicy directive. This is currently on our roadmap, but we haven't started implementing it yet.

Thanks so much for giving me a clearer understanding of what you're looking for.

@AnthonyMDev AnthonyMDev added this to the Minor Releases (1.x) milestone Mar 24, 2025
@comontes
Copy link
Author

Reading https://www.apollographql.com/docs/ios/caching/introduction I don't understand why it says "The favoriteBook field can now be queried for its title and yearPublished in a new query, and the normalized cache could return a response from the local cache immediately without needed to send the query to the server." without mentioning the @fieldPolicy directive. I'm really confused since this sentence directly confirms that a new query (FavoriteBookTitleAndYear) can be satisfied entirely from the cache (the merged data), avoiding a network request. This is the exact scenario I'm asking about - different queries using the same cached data and avoiding network requests. It's possible that @fieldPolicy is intended for a more advanced or different aspect of cache control? Are we talking about two different use cases?

@AnthonyMDev
Copy link
Contributor

The way it's described in the docs works only if we already know that the server has returned an object in the past for the field with that id.
When you run the LaunchList query, you'll get back a list of launches (let's say it's a list with a single record with id "123"). Then we create a cache record query.launches -> [$Launch:123]. But the cache stills doesn't have a record for the field launch(id: "123"). So when the launch(id: $launchId) is fetched in LaunchDetails with a given id of "123", it doesn't have a cache record. Once it has been fetched once, we create a cache record launch(id: "123") -> $Launch:123, and then it will hit the cache for future fetches, but we have to fetch that once to know that this connection exists.

The issue is that when resolving the launch(id: $launchId) field, the GraphQLExecutor doesn't actually know that the value passed into the $launchId parameter is the cache id of the object. You could also have a field like launch(named: $launchName), which your server would then resolve by looking up a launch by some other field value, or doing really any other arbitrary behavior that the client side cache doesn't know anything about. So we can't just assume the cache id of the object.

The @fieldPolicy client-side directive allows you to tell the cache "when I resolve this field, use the value of the $launchId parameter as the cache id". The client-side cache can then make the assumption that it's looking for an object with the cache key $Launch:123. This allows us to get around the need for that first network fetch of the LaunchDetails.

Effectively this directive gives you a way to tell the cache what the server-side behavior of that field resolver is.

@comontes
Copy link
Author

Thanks @AnthonyMDev for the explanation! Now, If I have a Mutable Fragment Definition, use it in multiple queries, and use ReadWriteTransaction.updateObject(ofType:withKey:variables:_:) to write data for a mutable fragment, could you clarify the current capabilities and limitations of this operation? Will it update cache for some queries, just previous queries that have used the same variable? If we do this and write data for a mutable fragment, we still need to write local cache mutations to the cache or not? is it just doing one or the other? sorry but I'm a bit confused 😵‍💫

@AnthonyMDev
Copy link
Contributor

It will update the fields on that object in the cache.

Say you call updateObject on a Launch object with id 123. The cache record for the key Launch:123 will have its fields updated.

Any field on another object that has been previously fetched and returned a Launch with the id 123 will have a cache record that references that object.

So your cache could have a set of records like this:

"QUERY_ROOT.launches": ["$Launch:123"],
"QUERY_ROOT.launch(id: "123"): "$Launch:123"],
"QUERY_ROOT.launch(id: "567"): "$Launch:567"],
"Launch:123": ["id": "123", "site": "Mars", ...],
"Launch:567": ["id": "567", "site": "Venus", ...],

If you still have questions, check out this article. It does a great job of explaining the underlying concepts of how the normalized cache works.

@AnthonyMDev AnthonyMDev added caching and removed bug Generally incorrect behavior needs investigation labels May 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants