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

RFC: Pagination helpers #38

Open
martinbonnin opened this issue Nov 23, 2021 · 14 comments
Open

RFC: Pagination helpers #38

martinbonnin opened this issue Nov 23, 2021 · 14 comments
Assignees
Labels
feedback wanted Please try the feature and give us feedback 🙏💜

Comments

@martinbonnin
Copy link
Contributor

martinbonnin commented Nov 23, 2021

In v0.0.1, a Pagination feature has been added.

It allows to have pages of data being automatically merged in a single field of a record, which allows to watch and update the UI as the list grows.

Pleas use this ticket to give any feedback or questions regarding that feature.

@sonatard
Copy link

sonatard commented Jan 5, 2022

And we need relayStylePagination helper function.
https://www.apollographql.com/docs/react/pagination/cursor-based/#relay-style-cursor-pagination

@pauldavies83
Copy link

We have a paginated query that returns us a list of our app items, like so…

query ItemsQuery($itemArgs: ItemArgs!) {
    items(args: $itemArgs) {
        edges {
            id
            node
        }
        pageInfo
    }
}

type ItemsArgs {
  first: Float
  after: String
  last: Float
  before: String
  filterBy: ItemsFilterByArgs
}

where the pageInfo contains the pagination info, and ItemsArgs allows us to request a “bucket” of data (usually a month of time’s worth).

The use case is for us to regularly sync our items from the server to the normalised cache, and our UI will watch the cache.
It seems the only way for us to do this is to iterate over the pages in the query, and manually writeOperation the cache to populate it with all the edges (and key it against the original query with no pagination parameter).

It would be great if there was internal workings in the apolloClient to help us manage this.

@pauldavies83
Copy link

Hey @martinbonnin 👋

Did this get prioritised internally? Just wondering if this is something we can expect in the short/medium term?

Thanks! 🙏

@martinbonnin
Copy link
Contributor Author

Yes, it's one of the next big thing on the todo list after apollographql/apollo-kotlin#3566. Pretty hard to commit to a date at this point but the coming months are the current goal.

@martinbonnin
Copy link
Contributor Author

martinbonnin commented Mar 15, 2024

Hi 👋

We started investigating these APIs. You can read more about it in the design document.

The tldr; is:

  1. Import the incubating artifact:
// build.gradle.kts

dependencies {
  implementation("com.apollographql.apollo3:apollo-normalized-cache-incubating")
}
  1. extend your schema:
extend type Query @typePolicy(connectionFields: "usersConnection")
  1. configure your ApolloStore:
val apolloStore = ApolloStore(
  normalizedCacheFactory = cacheFactory,
  cacheKeyGenerator = TypePolicyCacheKeyGenerator,
  metadataGenerator = ConnectionMetadataGenerator(Pagination.connectionTypes),
  apolloResolver = FieldPolicyApolloResolver,
  recordMerger = ConnectionRecordMerger
)

Updating the cache now merges new items with the existing ones

Warning

The persisted database format is different so you can't use it together with the non-incubating DB

  1. reading the cache now returns the full non-paginated list.

It's still the early days and too early to use in production but any feedback is warmly welcome.

@sonatard
Copy link

sonatard commented Mar 18, 2024

@martinbonnin Do you have plans to support the following format?

type UserConnection {
  pageInfo: PageInfo!
  nodes: [User!]!
}

Shopify and GitHub APIs have nodes in addition to edges.
In Modern Relay, Cursors are no longer used, and instead startCursor and endCursor in PageInfo are used. Currently, Edge are an unnecessary layer.

スクリーンショット 2024-03-18 22 08 45

スクリーンショット 2024-03-18 22 09 14

https://shopify.dev/docs/api/admin-graphql/2024-01/queries/customers#returns

@BoD
Copy link
Collaborator

BoD commented Mar 18, 2024

Hey @sonatard, this is great feedback! I've created issue apollographql/apollo-kotlin#5735 to follow-up on this. Short answer is the current system is flexible enough to manually configure it to work with queries selecting nodes instead of edges but it would be better if it just worked automatically with the dedicated support we have for Relay style - I think we can do it without too much hurdles.

@BoD BoD self-assigned this Apr 19, 2024
@martinbonnin martinbonnin transferred this issue from apollographql/apollo-kotlin Sep 19, 2024
@martinbonnin
Copy link
Contributor Author

Hi 👋 The apollo-kotlin-normalized-cache-incubating artifacts now support this.
Documentation is at https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/pagination.html.
Let us know what you think.

@sonatard
Copy link

sonatard commented Sep 20, 2024

@martinbonnin I tried it right away, and it's working smoothly even with nodes! Thank you!

  • collection.storefront.graphql
query StorefrontCollectionProductsPage($id: ID!, $sordKey: ProductCollectionSortKeys, $reverse: Boolean, $first: Int!, $after: String, $filters: [ProductFilter!]!, $countryCode: CountryCode!, $languageCode: LanguageCode!) @inContext(country: $countryCode, language: $languageCode) {
    collection(id: $id) {
        id
        title
        products(first: $first, sortKey: $sordKey, reverse: $reverse, after: $after, filters: $filters) {
            nodes {
                id
                ... ProductItemFragment
            }
            pageInfo {
                endCursor
                hasNextPage
            }
        }
    }
}
  • Collection.kt
                                        val afterQuery = q.copy(after = Optional.presentIfNotNull(collection.products.pageInfo.endCursor))
-                                        val resp = storefront.query(afterQuery).fetchPolicy(FetchPolicy.NetworkOnly).execute()
-                                        val respData = resp.data ?: return@LaunchedEffect
-                                        val respCollection = respData.collection ?: return@LaunchedEffect
-                                        val newNodes = collection.products.nodes + respCollection.products.nodes
-                                        val newData =
-                                            respData.copy(
-                                                collection =
-                                                    respCollection.copy(
-                                                        products = respCollection.products.copy(nodes = newNodes)
-                                                    )
-                                            )
-                                        val keys = storefront.apolloStore.writeOperation(q, newData)
-                                        storefront.apolloStore.publish(keys)
+                                        storefront.query(afterQuery).fetchPolicy(FetchPolicy.NetworkOnly).execute()
  • build.gradle.kts
-    implementation("com.apollographql.apollo:apollo-normalized-cache:$apolloVersion")
+    val apolloNormalizedCacheIncubating = "0.0.2"
+    implementation("com.apollographql.cache:normalized-cache-incubating:$apolloNormalizedCacheIncubating")
-        schemaFiles.from("src/main/java/com/xxx/android/storefront.graphqls")
+        schemaFiles.from("src/main/java/com/xxx/android/storefront.graphqls", "src/main/java/com/xxx/android/connection.storefront.graphqls")
  • connection.storefront.graphqls
+ extend type Collection @typePolicy(connectionFields: "products")
+ extend type Collection @fieldPolicy(forField: "products" paginationArgs: "after")
  • StorefrontApolloClient.kt
        .normalizedCache(
            normalizedCacheFactory = memoryCacheFactory,
            cacheKeyGenerator = IdCacheKeyGenerator,
-            cacheResolver = IdCacheResolver,
+            metadataGenerator = ConnectionMetadataGenerator(connectionTypes = connectionTypes),
+            recordMerger = ConnectionRecordMerger

@sonatard
Copy link

@martinbonnin The cacheResolver has been removed from normalizedCache. How can I use the id field as the key for the cache? Or will the id field automatically be used as the cache key without any configuration?

@BoD
Copy link
Collaborator

BoD commented Sep 20, 2024

@sonatard Glad that it works well for you! Also you should be able to remove extend type Collection @fieldPolicy(forField: "products" paginationArgs: "after") as it's done automatically with connectionFields.

About the cache resolver: it's still there but the API changed a bit: resolveField now takes a ResolverContext that contains the values that were previously passed as arguments separately - it should be straightforward to migrate to the new signature but don't hesitate to tell us if this isn't clear!

@sonatard
Copy link

sonatard commented Sep 20, 2024

@BoD

Also you should be able to remove extend type Collection @fieldPolicy(forField: "products" paginationArgs: "after") as it's done automatically with connectionFields

Thanks!! I removed it.

About the cache resolver: it's still there but the API changed a bit: resolveField now takes a ResolverContext that contains the values that were previously passed as arguments separately - it should be straightforward to migrate to the new signature but don't hesitate to tell us if this isn't clear!

I found this PR, but it seems it hasn't been released yet. Is there a planned release date?
https://github.com/apollographql/apollo-kotlin-normalized-cache-incubating/pull/20/files

I created an IdCacheResolver using ApolloResolver, but in the next release, ApolloResolver will be replaced with CacheResolver.

  • version 0.0.2
object IdCacheResolver : ApolloResolver {
    override fun resolveField(context: ResolverContext): Any? {
        val id = context.field.argumentValue("id", context.variables).getOrNull()?.toString()
        if (id != null) {
            return CacheKey(id)
        }

        return DefaultApolloResolver.resolveField(context)
    }
}
  • version 0.0.2
        .normalizedCache(
            normalizedCacheFactory = memoryCacheFactory,
            cacheKeyGenerator = IdCacheKeyGenerator,
-            cacheResolver = IdCacheResolver,
+            apolloResolver = IdCacheResolver,
+            metadataGenerator = ConnectionMetadataGenerator(connectionTypes = connectionTypes),
+            recordMerger = ConnectionRecordMerger

@BoD
Copy link
Collaborator

BoD commented Sep 20, 2024

Woops you are right @sonatard, sorry about the confusion, I forgot this hadn't been released yet. I just published a new release, v0.0.3, which contains the change. It should be available in a few minutes.

@sonatard
Copy link

sonatard commented Sep 21, 2024

@BoD Thank you for your prompt response. It turned out to be a simple diff.

  • version 0.0.3
object IdCacheResolver : CacheResolver {
    override fun resolveField(context: ResolverContext): Any? {
        val id =
            context.field
                .argumentValue("id", context.variables)
                .getOrNull()
                ?.toString()
        if (id != null) {
            return CacheKey(id)
        }

        return DefaultCacheResolver.resolveField(context)
    }
}
        .normalizedCache(
            normalizedCacheFactory = memoryCacheFactory,
            cacheKeyGenerator = IdCacheKeyGenerator,
            cacheResolver = IdCacheResolver,
+            metadataGenerator = ConnectionMetadataGenerator(connectionTypes = connectionTypes),
+            recordMerger = ConnectionRecordMerger

Everything worked perfectly. Thank you for the excellent work. I am looking forward to its official release.

@martinbonnin martinbonnin added the feedback wanted Please try the feature and give us feedback 🙏💜 label Oct 3, 2024
@martinbonnin martinbonnin pinned this issue Oct 17, 2024
@martinbonnin martinbonnin changed the title Pagination helpers RFC: Pagination helpers Oct 17, 2024
@sonatard sonatard mentioned this issue Nov 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feedback wanted Please try the feature and give us feedback 🙏💜
Projects
None yet
Development

No branches or pull requests

4 participants