From df6b1aac14ae106a8ae948cb222201a8d103802d Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 11 Oct 2024 18:23:49 +0200 Subject: [PATCH 1/4] Add IdCacheKeyGenerator and IdCacheKeyResolver --- CHANGELOG.md | 3 +- .../cache/normalized/api/CacheKeyGenerator.kt | 18 +++++++++++- .../cache/normalized/api/CacheKeyResolver.kt | 29 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 677bfe4f..fed56b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ - Expiration support (see [the documentation](https://apollographql.github.io/apollo-kotlin-normalized-cache-incubating/expiration.html) for details) - Compatibility with the IntelliJ plugin cache viewer (#42) - For consistency, `MemoryCacheFactory` and `MemoryCache` are now in the `com.apollographql.cache.normalized.memory` package -- Remove deprecated symbols +- Remove deprecated symbols +- Add `IdCacheKeyGenerator` and `IdCacheKeyResolver` (#41) # Version 0.0.3 _2024-09-20_ diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyGenerator.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyGenerator.kt index faf4e6f7..91df0b6f 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyGenerator.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyGenerator.kt @@ -41,7 +41,7 @@ class CacheKeyGeneratorContext( ) /** - * A [CacheKeyGenerator] that uses annotations to compute the id + * A [CacheKeyGenerator] that uses the `@typePolicy` directive to compute the id */ object TypePolicyCacheKeyGenerator : CacheKeyGenerator { override fun cacheKeyForObject(obj: Map, context: CacheKeyGeneratorContext): CacheKey? { @@ -54,3 +54,19 @@ object TypePolicyCacheKeyGenerator : CacheKeyGenerator { } } } + +/** + * A [CacheKeyGenerator] that uses the given id fields to compute the cache key. + * If the id field(s) is/are missing, the object is considered to not have an id. + * + * @see IdCacheKeyResolver + */ +class IdCacheKeyGenerator(private vararg val idFields: String = arrayOf("id")) : CacheKeyGenerator { + override fun cacheKeyForObject(obj: Map, context: CacheKeyGeneratorContext): CacheKey? { + val values = idFields.map { + (obj[it] ?: return null).toString() + } + val typeName = context.field.type.rawType().name + return CacheKey(typeName, values) + } +} diff --git a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyResolver.kt b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyResolver.kt index 2d0c5e0c..247d8dc4 100644 --- a/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyResolver.kt +++ b/normalized-cache-incubating/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheKeyResolver.kt @@ -66,3 +66,32 @@ abstract class CacheKeyResolver : CacheResolver { return DefaultCacheResolver.resolveField(context) } } + +/** + * A simple [CacheKeyResolver] that uses the id/ids argument, if present, to compute the cache key. + * The name of the id arguments can be provided (by default "id" for objects and "ids" for lists). + * If several names are provided, the first present one is used. + * Only one level of list is supported - implement [CacheResolver] if you need arbitrary nested lists of objects. + * + * @param idFields possible names of the argument containing the id for objects + * @param idListFields possible names of the argument containing the ids for lists + * + * @see IdCacheKeyGenerator + */ +class IdCacheKeyResolver( + private val idFields: List = listOf("id"), + private val idListFields: List = listOf("ids"), +) : CacheKeyResolver() { + override fun cacheKeyForField(context: ResolverContext): CacheKey? { + val typeName = context.field.type.rawType().name + val id = idFields.firstNotNullOfOrNull { context.field.argumentValue(it, context.variables).getOrNull()?.toString() } ?: return null + return CacheKey(typeName, id) + } + + override fun listOfCacheKeysForField(context: ResolverContext): List? { + val typeName = context.field.type.rawType().name + val ids = idListFields.firstNotNullOfOrNull { context.field.argumentValue(it, context.variables).getOrNull() as? List<*> } + ?: return null + return ids.map { id -> id?.toString()?.let { CacheKey(typeName, it) } } + } +} From 3521783534bc272eee357ccbd8a825d71f6dac87 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 14 Oct 2024 15:01:27 +0200 Subject: [PATCH 2/4] Add IdCacheKeyGenerator and IdCacheKeyResolver --- tests/normalized-cache/build.gradle.kts | 2 +- .../src/commonMain/graphql/operations.graphql | 32 ++++++++ .../src/commonMain/graphql/schema.graphqls | 6 ++ .../kotlin/IdCacheKeyGeneratorTest.kt | 81 +++++++++++++++++++ .../commonTest/kotlin/MemoryCacheOnlyTest.kt | 1 - .../commonTest/kotlin/NormalizationTest.kt | 1 - 6 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt diff --git a/tests/normalized-cache/build.gradle.kts b/tests/normalized-cache/build.gradle.kts index 1b620954..9d521fb6 100644 --- a/tests/normalized-cache/build.gradle.kts +++ b/tests/normalized-cache/build.gradle.kts @@ -64,6 +64,6 @@ kotlin { apollo { service("service") { - packageName.set("sqlite") + packageName.set("test") } } diff --git a/tests/normalized-cache/src/commonMain/graphql/operations.graphql b/tests/normalized-cache/src/commonMain/graphql/operations.graphql index 5f2ca126..2b0280bb 100644 --- a/tests/normalized-cache/src/commonMain/graphql/operations.graphql +++ b/tests/normalized-cache/src/commonMain/graphql/operations.graphql @@ -5,6 +5,38 @@ query GetUser { } } +query GetUser2($id: ID!) { + user2(id: $id) { + id + name + email + } +} + +query GetUserById($userId: ID!) { + userById(userId: $userId) { + userId + name + email + } +} + +query GetUsers($ids: [ID!]!) { + users(ids: $ids) { + id + name + email + } +} + +query GetUsersByIDs($userIds: [ID!]!) { + usersByIDs(userIds: $userIds) { + id + name + email + } +} + query RepositoryListQuery($first: Int = 15, $after: String) { repositories(first: $first, after: $after) { id diff --git a/tests/normalized-cache/src/commonMain/graphql/schema.graphqls b/tests/normalized-cache/src/commonMain/graphql/schema.graphqls index c902e2c2..d82a5d02 100644 --- a/tests/normalized-cache/src/commonMain/graphql/schema.graphqls +++ b/tests/normalized-cache/src/commonMain/graphql/schema.graphqls @@ -1,9 +1,15 @@ type Query { user: User + user2(id: ID!): User + userById(userId: ID!): User + users(ids: [ID!]!): [User!]! + usersByIDs(userIds: [ID!]!): [User!]! repositories(first: Int, after: String): [Repository!]! } type User { + id: ID! + userId: ID! name: String! email: String! admin: Boolean diff --git a/tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt b/tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt new file mode 100644 index 00000000..6e3580d5 --- /dev/null +++ b/tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt @@ -0,0 +1,81 @@ +package test + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.testing.QueueTestNetworkTransport +import com.apollographql.apollo.testing.enqueueTestResponse +import com.apollographql.apollo.testing.internal.runTest +import com.apollographql.cache.normalized.ApolloStore +import com.apollographql.cache.normalized.FetchPolicy +import com.apollographql.cache.normalized.api.IdCacheKeyGenerator +import com.apollographql.cache.normalized.api.IdCacheKeyResolver +import com.apollographql.cache.normalized.fetchPolicy +import com.apollographql.cache.normalized.memory.MemoryCacheFactory +import com.apollographql.cache.normalized.store +import kotlin.test.Test +import kotlin.test.assertEquals + +class IdCacheKeyGeneratorTest { + @Test + fun defaultValues() = runTest { + val store = ApolloStore( + normalizedCacheFactory = MemoryCacheFactory(), + cacheKeyGenerator = IdCacheKeyGenerator(), + cacheResolver = IdCacheKeyResolver(), + ) + val apolloClient = ApolloClient.Builder().networkTransport(QueueTestNetworkTransport()).store(store).build() + val query = GetUser2Query("42") + apolloClient.enqueueTestResponse(query, GetUser2Query.Data(GetUser2Query.User2(id = "42", name = "John", email = "a@a.com"))) + apolloClient.query(query).fetchPolicy(FetchPolicy.NetworkOnly).execute() + val user = apolloClient.query(query).fetchPolicy(FetchPolicy.CacheOnly).execute().dataOrThrow().user2!! + assertEquals("John", user.name) + assertEquals("a@a.com", user.email) + } + + @Test + fun customIdField() = runTest { + val store = ApolloStore( + normalizedCacheFactory = MemoryCacheFactory(), + cacheKeyGenerator = IdCacheKeyGenerator("userId"), + cacheResolver = IdCacheKeyResolver(idFields = listOf("userId")), + ) + val apolloClient = ApolloClient.Builder().networkTransport(QueueTestNetworkTransport()).store(store).build() + val query = GetUserByIdQuery("42") + apolloClient.enqueueTestResponse(query, GetUserByIdQuery.Data(GetUserByIdQuery.UserById(userId = "42", name = "John", email = "a@a.com"))) + apolloClient.query(query).fetchPolicy(FetchPolicy.NetworkOnly).execute() + val user = apolloClient.query(query).fetchPolicy(FetchPolicy.CacheOnly).execute().dataOrThrow().userById!! + assertEquals("John", user.name) + assertEquals("a@a.com", user.email) + } + + @Test + fun lists() = runTest { + val store = ApolloStore( + normalizedCacheFactory = MemoryCacheFactory(), + cacheKeyGenerator = IdCacheKeyGenerator("id"), + cacheResolver = IdCacheKeyResolver(idListFields = listOf("ids", "userIds")), + ) + val apolloClient = ApolloClient.Builder().networkTransport(QueueTestNetworkTransport()).store(store).build() + val query1 = GetUsersQuery(listOf("42", "43")) + apolloClient.enqueueTestResponse(query1, GetUsersQuery.Data(listOf( + GetUsersQuery.User(id = "42", name = "John", email = "a@a.com"), + GetUsersQuery.User(id = "43", name = "Jane", email = "b@b.com"), + ) + ) + ) + apolloClient.query(query1).fetchPolicy(FetchPolicy.NetworkOnly).execute() + val users1 = apolloClient.query(query1).fetchPolicy(FetchPolicy.CacheOnly).execute().dataOrThrow().users + assertEquals(2, users1.size) + assertEquals("John", users1[0].name) + assertEquals("a@a.com", users1[0].email) + assertEquals("Jane", users1[1].name) + assertEquals("b@b.com", users1[1].email) + + val query2 = GetUsersByIDsQuery(listOf("42", "43")) + val users2 = apolloClient.query(query2).fetchPolicy(FetchPolicy.CacheOnly).execute().dataOrThrow().usersByIDs + assertEquals(2, users2.size) + assertEquals("John", users2[0].name) + assertEquals("a@a.com", users2[0].email) + assertEquals("Jane", users2[1].name) + assertEquals("b@b.com", users2[1].email) + } +} diff --git a/tests/normalized-cache/src/commonTest/kotlin/MemoryCacheOnlyTest.kt b/tests/normalized-cache/src/commonTest/kotlin/MemoryCacheOnlyTest.kt index ab546b77..b42b4e79 100644 --- a/tests/normalized-cache/src/commonTest/kotlin/MemoryCacheOnlyTest.kt +++ b/tests/normalized-cache/src/commonTest/kotlin/MemoryCacheOnlyTest.kt @@ -15,7 +15,6 @@ import com.apollographql.cache.normalized.memoryCacheOnly import com.apollographql.cache.normalized.sql.SqlNormalizedCache import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory import com.apollographql.cache.normalized.store -import sqlite.GetUserQuery import kotlin.reflect.KClass import kotlin.test.Test import kotlin.test.assertEquals diff --git a/tests/normalized-cache/src/commonTest/kotlin/NormalizationTest.kt b/tests/normalized-cache/src/commonTest/kotlin/NormalizationTest.kt index 1f9d03b6..539c25cb 100644 --- a/tests/normalized-cache/src/commonTest/kotlin/NormalizationTest.kt +++ b/tests/normalized-cache/src/commonTest/kotlin/NormalizationTest.kt @@ -8,7 +8,6 @@ import com.apollographql.cache.normalized.FetchPolicy import com.apollographql.cache.normalized.fetchPolicy import com.apollographql.cache.normalized.memory.MemoryCacheFactory import com.apollographql.cache.normalized.normalizedCache -import sqlite.RepositoryListQuery import kotlin.test.Test import kotlin.test.assertEquals From 3d2a00bf03f9fedbb45424bed84cda7b0c0e5883 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 14 Oct 2024 15:23:39 +0200 Subject: [PATCH 3/4] Fix weird formatting --- .../commonTest/kotlin/IdCacheKeyGeneratorTest.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt b/tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt index 6e3580d5..a7e3b76b 100644 --- a/tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt +++ b/tests/normalized-cache/src/commonTest/kotlin/IdCacheKeyGeneratorTest.kt @@ -56,11 +56,14 @@ class IdCacheKeyGeneratorTest { ) val apolloClient = ApolloClient.Builder().networkTransport(QueueTestNetworkTransport()).store(store).build() val query1 = GetUsersQuery(listOf("42", "43")) - apolloClient.enqueueTestResponse(query1, GetUsersQuery.Data(listOf( - GetUsersQuery.User(id = "42", name = "John", email = "a@a.com"), - GetUsersQuery.User(id = "43", name = "Jane", email = "b@b.com"), - ) - ) + apolloClient.enqueueTestResponse( + query1, + GetUsersQuery.Data( + listOf( + GetUsersQuery.User(id = "42", name = "John", email = "a@a.com"), + GetUsersQuery.User(id = "43", name = "Jane", email = "b@b.com"), + ) + ) ) apolloClient.query(query1).fetchPolicy(FetchPolicy.NetworkOnly).execute() val users1 = apolloClient.query(query1).fetchPolicy(FetchPolicy.CacheOnly).execute().dataOrThrow().users From 626b7885bec7848e8f0efa7d5fbe90b11d48ae5a Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 14 Oct 2024 15:27:29 +0200 Subject: [PATCH 4/4] Update api dump --- .../api/normalized-cache-incubating.api | 15 +++++++++++++++ .../api/normalized-cache-incubating.klib.api | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.api b/normalized-cache-incubating/api/normalized-cache-incubating.api index ea25d022..0a0fc53c 100644 --- a/normalized-cache-incubating/api/normalized-cache-incubating.api +++ b/normalized-cache-incubating/api/normalized-cache-incubating.api @@ -304,6 +304,21 @@ public final class com/apollographql/cache/normalized/api/GlobalMaxAgeProvider : public fun getMaxAge-5sfh64U (Lcom/apollographql/cache/normalized/api/MaxAgeContext;)J } +public final class com/apollographql/cache/normalized/api/IdCacheKeyGenerator : com/apollographql/cache/normalized/api/CacheKeyGenerator { + public fun ()V + public fun ([Ljava/lang/String;)V + public synthetic fun ([Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun cacheKeyForObject (Ljava/util/Map;Lcom/apollographql/cache/normalized/api/CacheKeyGeneratorContext;)Lcom/apollographql/cache/normalized/api/CacheKey; +} + +public final class com/apollographql/cache/normalized/api/IdCacheKeyResolver : com/apollographql/cache/normalized/api/CacheKeyResolver { + public fun ()V + public fun (Ljava/util/List;Ljava/util/List;)V + public synthetic fun (Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun cacheKeyForField (Lcom/apollographql/cache/normalized/api/ResolverContext;)Lcom/apollographql/cache/normalized/api/CacheKey; + public fun listOfCacheKeysForField (Lcom/apollographql/cache/normalized/api/ResolverContext;)Ljava/util/List; +} + public abstract interface class com/apollographql/cache/normalized/api/MaxAge { } diff --git a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api index 3fbbe082..8c4aa683 100644 --- a/normalized-cache-incubating/api/normalized-cache-incubating.klib.api +++ b/normalized-cache-incubating/api/normalized-cache-incubating.klib.api @@ -167,6 +167,15 @@ final class com.apollographql.cache.normalized.api/GlobalMaxAgeProvider : com.ap constructor (kotlin.time/Duration) // com.apollographql.cache.normalized.api/GlobalMaxAgeProvider.|(kotlin.time.Duration){}[0] final fun getMaxAge(com.apollographql.cache.normalized.api/MaxAgeContext): kotlin.time/Duration // com.apollographql.cache.normalized.api/GlobalMaxAgeProvider.getMaxAge|getMaxAge(com.apollographql.cache.normalized.api.MaxAgeContext){}[0] } +final class com.apollographql.cache.normalized.api/IdCacheKeyGenerator : com.apollographql.cache.normalized.api/CacheKeyGenerator { // com.apollographql.cache.normalized.api/IdCacheKeyGenerator|null[0] + constructor (kotlin/Array... = ...) // com.apollographql.cache.normalized.api/IdCacheKeyGenerator.|(kotlin.Array...){}[0] + final fun cacheKeyForObject(kotlin.collections/Map, com.apollographql.cache.normalized.api/CacheKeyGeneratorContext): com.apollographql.cache.normalized.api/CacheKey? // com.apollographql.cache.normalized.api/IdCacheKeyGenerator.cacheKeyForObject|cacheKeyForObject(kotlin.collections.Map;com.apollographql.cache.normalized.api.CacheKeyGeneratorContext){}[0] +} +final class com.apollographql.cache.normalized.api/IdCacheKeyResolver : com.apollographql.cache.normalized.api/CacheKeyResolver { // com.apollographql.cache.normalized.api/IdCacheKeyResolver|null[0] + constructor (kotlin.collections/List = ..., kotlin.collections/List = ...) // com.apollographql.cache.normalized.api/IdCacheKeyResolver.|(kotlin.collections.List;kotlin.collections.List){}[0] + final fun cacheKeyForField(com.apollographql.cache.normalized.api/ResolverContext): com.apollographql.cache.normalized.api/CacheKey? // com.apollographql.cache.normalized.api/IdCacheKeyResolver.cacheKeyForField|cacheKeyForField(com.apollographql.cache.normalized.api.ResolverContext){}[0] + final fun listOfCacheKeysForField(com.apollographql.cache.normalized.api/ResolverContext): kotlin.collections/List? // com.apollographql.cache.normalized.api/IdCacheKeyResolver.listOfCacheKeysForField|listOfCacheKeysForField(com.apollographql.cache.normalized.api.ResolverContext){}[0] +} final class com.apollographql.cache.normalized.api/MaxAgeContext { // com.apollographql.cache.normalized.api/MaxAgeContext|null[0] constructor (kotlin.collections/List) // com.apollographql.cache.normalized.api/MaxAgeContext.|(kotlin.collections.List){}[0] final val fieldPath // com.apollographql.cache.normalized.api/MaxAgeContext.fieldPath|{}fieldPath[0]