Skip to content

Commit 720d01d

Browse files
authored
Add a CacheKeyApolloResolver based on ApolloResolver (incubating) (#5970)
1 parent e0a7c2b commit 720d01d

File tree

4 files changed

+192
-21
lines changed

4 files changed

+192
-21
lines changed

docs/source/caching/programmatic-ids.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ You can get the current object's typename from the `context` object and include
4343
```kotlin
4444
val cacheKeyGenerator = object : CacheKeyGenerator {
4545
override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
46-
val typename = context.field.type.leafType().name
46+
val typename = context.field.type.rawType().name
4747
val id = obj["id"] as String
4848

4949
return CacheKey(typename, id)
@@ -54,7 +54,7 @@ val cacheKeyGenerator = object : CacheKeyGenerator {
5454
You can also use the current object's typename to use _different_ cache ID generation logic for different object types.
5555

5656
Note that for cache ID generation to work, your GraphQL operations must return whatever fields your custom code relies on (such as `id` above). If a query does not return a required field, the cache ID will be inconsistent, resulting in data duplication.
57-
Also, using `context.field.type.leafType().name` yields the typename of an Union as opposed to the expected runtime value of the type received in the response. Instead querying for the `__typename` is safer.
57+
Also, for interfaces and unions, `context.field.type.rawType().name` yields the typename as it is declared in the schema, as opposed to the runtime value of the type received in the response. Instead querying for the `__typename` is safer.
5858
To make sure `__typename` is included in all operations set the [addTypename](https://www.apollographql.com/docs/kotlin/kdoc/apollo-gradle-plugin-external/com.apollographql.apollo3.gradle.api/-service/add-typename.html) gradle config:
5959

6060
```
@@ -73,8 +73,8 @@ The `CacheKeyResolver` class enables you to generate custom cache IDs from a fie
7373
val cacheKeyResolver = object: CacheKeyResolver() {
7474
override fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey? {
7575
// [field] contains compile-time information about what type of object is being resolved.
76-
// Even though we call leafType() here, we're guaranteed that the type is a composite type and not a list
77-
val typename = field.type.leafType().name
76+
// Even though we call rawType() here, we're guaranteed that the type is a composite type and not a list
77+
val typename = field.type.rawType().name
7878

7979
// argumentValue returns the runtime value of the "id" argument
8080
// from either the variables or as a literal value
@@ -127,7 +127,7 @@ To have the cache look up _all_ books in the `ids` list, we need to override `li
127127
```kotlin
128128
override fun listOfCacheKeysForField(field: CompiledField, variables: Executable.Variables): List<CacheKey?>? {
129129
// Note that the field *can* be a list type here
130-
val typename = field.type.leafType().name
130+
val typename = field.type.rawType().name
131131

132132
// argumentValue returns the runtime value of the "id" argument
133133
// from either the variables or as a literal value

libraries/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/CacheKeyResolver.kt

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.apollographql.apollo3.cache.normalized.api
22

3+
import com.apollographql.apollo3.annotations.ApolloExperimental
34
import com.apollographql.apollo3.api.CompiledField
45
import com.apollographql.apollo3.api.CompiledListType
56
import com.apollographql.apollo3.api.CompiledNamedType
@@ -9,31 +10,31 @@ import com.apollographql.apollo3.api.isComposite
910
import kotlin.jvm.JvmSuppressWildcards
1011

1112
/**
12-
* A [CacheResolver] that resolves objects and list of objects and fallbacks to the default resolver for scalar fields.
13+
* A [CacheResolver] that resolves objects and list of objects and falls back to the default resolver for scalar fields.
1314
* It is intended to simplify the usage of [CacheResolver] when no special handling is needed for scalar fields.
1415
*
1516
* Override [cacheKeyForField] to compute a cache key for a field of composite type.
1617
* Override [listOfCacheKeysForField] to compute a list of cache keys for a field of 'list-of-composite' type.
1718
*
18-
* For simplicity, this only handles one level of list. Implement [CacheResolver] if you need arbitrary nested lists of objects.
19+
* For simplicity, this only handles one level of lists. Implement [CacheResolver] if you need arbitrary nested lists of objects.
1920
*/
2021
abstract class CacheKeyResolver : CacheResolver {
2122
/**
22-
* Return the computed the cache key for a composite field.
23+
* Returns the computed cache key for a composite field.
2324
*
24-
* If the field is of object type, you can get the object typename with `field.type.rawType().name`
25-
* If the field is of interface type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
25+
* If the field is of object type, you can get the object typename with `field.type.rawType().name`.
26+
* If the field is of interface or union type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
2627
* in the whole schema as it cannot be namespaced by the typename anymore.
2728
*
2829
* If the returned [CacheKey] is null, the resolver will use the default handling and use any previously cached value.
2930
*/
3031
abstract fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey?
3132

3233
/**
33-
* For a field that contains a list of objects, [listOfCacheKeysForField ] returns a list of [CacheKey]s where each [CacheKey] identifies an object.
34+
* For a field that contains a list of objects, [listOfCacheKeysForField] returns a list of [CacheKey]s where each [CacheKey] identifies an object.
3435
*
35-
* If the field is of object type, you can get the object typename with `field.type.rawType().name`
36-
* If the field is of interface type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
36+
* If the field is of object type, you can get the object typename with `field.type.rawType().name`.
37+
* If the field is of interface or union type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
3738
* in the whole schema as it cannot be namespaced by the typename anymore.
3839
*
3940
* If an individual [CacheKey] is null, the resulting object will be null in the response.
@@ -74,3 +75,66 @@ abstract class CacheKeyResolver : CacheResolver {
7475
return DefaultCacheResolver.resolveField(field, variables, parent, parentId)
7576
}
7677
}
78+
79+
/**
80+
* An [ApolloResolver] that resolves objects and list of objects and falls back to the default resolver for scalar fields.
81+
* It is intended to simplify the usage of [ApolloResolver] when no special handling is needed for scalar fields.
82+
*
83+
* Override [cacheKeyForField] to compute a cache key for a field of composite type.
84+
* Override [listOfCacheKeysForField] to compute a list of cache keys for a field of 'list-of-composite' type.
85+
*
86+
* For simplicity, this only handles one level of lists. Implement [ApolloResolver] if you need arbitrary nested lists of objects.
87+
*/
88+
@ApolloExperimental
89+
abstract class CacheKeyApolloResolver : ApolloResolver {
90+
/**
91+
* Returns the computed cache key for a composite field.
92+
*
93+
* If the field is of object type, you can get the object typename with `field.type.rawType().name`.
94+
* If the field is of interface or union type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
95+
* in the whole schema as it cannot be namespaced by the typename anymore.
96+
*
97+
* If the returned [CacheKey] is null, the resolver will use the default handling and use any previously cached value.
98+
*/
99+
abstract fun cacheKeyForField(context: ResolverContext): CacheKey?
100+
101+
/**
102+
* For a field that contains a list of objects, [listOfCacheKeysForField] returns a list of [CacheKey]s where each [CacheKey] identifies an object.
103+
*
104+
* If the field is of object type, you can get the object typename with `field.type.rawType().name`.
105+
* If the field is of interface or union type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
106+
* in the whole schema as it cannot be namespaced by the typename anymore.
107+
*
108+
* If an individual [CacheKey] is null, the resulting object will be null in the response.
109+
* If the returned list of [CacheKey]s is null, the resolver will use the default handling and use any previously cached value.
110+
*/
111+
open fun listOfCacheKeysForField(context: ResolverContext): List<CacheKey?>? = null
112+
113+
final override fun resolveField(context: ResolverContext): Any? {
114+
var type = context.field.type
115+
if (type is CompiledNotNullType) {
116+
type = type.ofType
117+
}
118+
if (type is CompiledNamedType && type.isComposite()) {
119+
val result = cacheKeyForField(context)
120+
if (result != null) {
121+
return result
122+
}
123+
}
124+
125+
if (type is CompiledListType) {
126+
type = type.ofType
127+
if (type is CompiledNotNullType) {
128+
type = type.ofType
129+
}
130+
if (type is CompiledNamedType && type.isComposite()) {
131+
val result = listOfCacheKeysForField(context)
132+
if (result != null) {
133+
return result
134+
}
135+
}
136+
}
137+
138+
return DefaultApolloResolver.resolveField(context)
139+
}
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.apollographql.apollo3.cache.normalized
2+
3+
import com.apollographql.apollo3.api.CompiledField
4+
import com.apollographql.apollo3.api.CompiledListType
5+
import com.apollographql.apollo3.api.Executable
6+
import com.apollographql.apollo3.api.ObjectType
7+
import com.apollographql.apollo3.cache.normalized.CacheKeyApolloResolverTest.Fixtures.TEST_LIST_FIELD
8+
import com.apollographql.apollo3.cache.normalized.CacheKeyApolloResolverTest.Fixtures.TEST_SIMPLE_FIELD
9+
import com.apollographql.apollo3.cache.normalized.api.CacheHeaders
10+
import com.apollographql.apollo3.cache.normalized.api.CacheKey
11+
import com.apollographql.apollo3.cache.normalized.api.CacheKeyApolloResolver
12+
import com.apollographql.apollo3.cache.normalized.api.DefaultFieldKeyGenerator
13+
import com.apollographql.apollo3.cache.normalized.api.ResolverContext
14+
import com.apollographql.apollo3.exception.CacheMissException
15+
import kotlin.test.BeforeTest
16+
import kotlin.test.Test
17+
import kotlin.test.assertEquals
18+
import kotlin.test.assertFailsWith
19+
import kotlin.test.fail
20+
21+
22+
class CacheKeyApolloResolverTest {
23+
24+
private lateinit var subject: CacheKeyApolloResolver
25+
lateinit var onCacheKeyForField: (context: ResolverContext) -> CacheKey?
26+
lateinit var onListOfCacheKeysForField: (context: ResolverContext) -> List<CacheKey?>?
27+
28+
@BeforeTest
29+
fun setup() {
30+
subject = FakeCacheKeyApolloResolver()
31+
onCacheKeyForField = { _ ->
32+
fail("Unexpected call to cacheKeyForField")
33+
}
34+
onListOfCacheKeysForField = { _ ->
35+
fail("Unexpected call to listOfCacheKeysForField")
36+
}
37+
}
38+
39+
private fun resolverContext(field: CompiledField) =
40+
ResolverContext(field, Executable.Variables(emptyMap()), emptyMap(), "", "", CacheHeaders(emptyMap()), DefaultFieldKeyGenerator)
41+
42+
@Test
43+
fun verify_cacheKeyForField_called_for_named_composite_field() {
44+
val expectedKey = CacheKey("test")
45+
val fields = mutableListOf<CompiledField>()
46+
47+
onCacheKeyForField = { context: ResolverContext ->
48+
fields += context.field
49+
expectedKey
50+
}
51+
52+
val returned = subject.resolveField(resolverContext(TEST_SIMPLE_FIELD))
53+
54+
assertEquals(returned, expectedKey)
55+
assertEquals(fields[0], TEST_SIMPLE_FIELD)
56+
}
57+
58+
@Test
59+
fun listOfCacheKeysForField_called_for_list_field() {
60+
val expectedKeys = listOf(CacheKey("test"))
61+
val fields = mutableListOf<CompiledField>()
62+
63+
onListOfCacheKeysForField = { context: ResolverContext ->
64+
fields += context.field
65+
expectedKeys
66+
}
67+
68+
val returned = subject.resolveField(resolverContext(TEST_LIST_FIELD))
69+
70+
assertEquals(returned, expectedKeys)
71+
assertEquals(fields[0], TEST_LIST_FIELD)
72+
}
73+
74+
@Test
75+
fun super_called_for_null_return_values() {
76+
onCacheKeyForField = { _ -> null }
77+
onListOfCacheKeysForField = { _ -> null }
78+
79+
// The best way to ensure that super was called is to check for a cache miss exception from CacheResolver()
80+
assertFailsWith<CacheMissException> {
81+
subject.resolveField(resolverContext(TEST_SIMPLE_FIELD))
82+
}
83+
assertFailsWith<CacheMissException> {
84+
subject.resolveField(resolverContext(TEST_LIST_FIELD))
85+
}
86+
}
87+
88+
inner class FakeCacheKeyApolloResolver : CacheKeyApolloResolver() {
89+
90+
override fun cacheKeyForField(context: ResolverContext): CacheKey? {
91+
return onCacheKeyForField(context)
92+
}
93+
94+
override fun listOfCacheKeysForField(context: ResolverContext): List<CacheKey?>? {
95+
return onListOfCacheKeysForField(context)
96+
}
97+
}
98+
99+
object Fixtures {
100+
101+
private val TEST_TYPE = ObjectType.Builder(name = "Test").keyFields(keyFields = listOf("id")).build()
102+
103+
val TEST_SIMPLE_FIELD = CompiledField.Builder(name = "test", type = TEST_TYPE).build()
104+
105+
val TEST_LIST_FIELD = CompiledField.Builder(name = "testList", type = CompiledListType(ofType = TEST_TYPE)).build()
106+
}
107+
}

libraries/apollo-normalized-cache-api/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/CacheKeyResolver.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,31 @@ import com.apollographql.apollo3.api.isComposite
99
import kotlin.jvm.JvmSuppressWildcards
1010

1111
/**
12-
* A [CacheResolver] that resolves objects and list of objects and fallbacks to the default resolver for scalar fields.
12+
* A [CacheResolver] that resolves objects and list of objects and falls back to the default resolver for scalar fields.
1313
* It is intended to simplify the usage of [CacheResolver] when no special handling is needed for scalar fields.
1414
*
1515
* Override [cacheKeyForField] to compute a cache key for a field of composite type.
1616
* Override [listOfCacheKeysForField] to compute a list of cache keys for a field of 'list-of-composite' type.
1717
*
18-
* For simplicity, this only handles one level of list. Implement [CacheResolver] if you need arbitrary nested lists of objects.
18+
* For simplicity, this only handles one level of lists. Implement [CacheResolver] if you need arbitrary nested lists of objects.
1919
*/
2020
abstract class CacheKeyResolver : CacheResolver {
2121
/**
22-
* Return the computed the cache key for a composite field.
22+
* Returns the computed cache key for a composite field.
2323
*
24-
* If the field is of object type, you can get the object typename with `field.type.rawType().name`
25-
* If the field is of interface type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
24+
* If the field is of object type, you can get the object typename with `field.type.rawType().name`.
25+
* If the field is of interface or union type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
2626
* in the whole schema as it cannot be namespaced by the typename anymore.
2727
*
2828
* If the returned [CacheKey] is null, the resolver will use the default handling and use any previously cached value.
2929
*/
3030
abstract fun cacheKeyForField(field: CompiledField, variables: Executable.Variables): CacheKey?
3131

3232
/**
33-
* For a field that contains a list of objects, [listOfCacheKeysForField ] returns a list of [CacheKey]s where each [CacheKey] identifies an object.
33+
* For a field that contains a list of objects, [listOfCacheKeysForField] returns a list of [CacheKey]s where each [CacheKey] identifies an object.
3434
*
35-
* If the field is of object type, you can get the object typename with `field.type.rawType().name`
36-
* If the field is of interface type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
35+
* If the field is of object type, you can get the object typename with `field.type.rawType().name`.
36+
* If the field is of interface or union type, the concrete object typename is not predictable and the returned [CacheKey] must be unique
3737
* in the whole schema as it cannot be namespaced by the typename anymore.
3838
*
3939
* If an individual [CacheKey] is null, the resulting object will be null in the response.

0 commit comments

Comments
 (0)