Skip to content

Commit 461c5f7

Browse files
authored
Fix #2818 (#4358)
* Fix overwrites cache entries when using fragments * Add integration test for fragment overwrites fix * Avoid using method not available before API level 24 on Android * Add assertion to check that the fragment is also correctly written
1 parent 66cd446 commit 461c5f7

File tree

6 files changed

+160
-4
lines changed

6 files changed

+160
-4
lines changed

apollo-integration/build.gradle.kts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ configure<ApolloExtension> {
5757
rootPackageName.set("com.apollographql.apollo.integration.directives")
5858
generateKotlinModels.set(true)
5959
}
60+
service("fragmentoverwrites") {
61+
sourceFolder.set("com/apollographql/apollo/integration/fragmentoverwrites")
62+
rootPackageName.set("com.apollographql.apollo.integration.fragmentoverwrites")
63+
generateKotlinModels.set(true)
64+
}
6065
service("sealedclasses") {
6166
sealedClassesForEnumsMatching.set(listOf(".*"))
6267
generateKotlinModels.set(true)
@@ -86,4 +91,4 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
8691
kotlinOptions {
8792
jvmTarget = JavaVersion.VERSION_1_8.toString()
8893
}
89-
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
query Home {
2+
home {
3+
...sectionFragment
4+
sectionA {
5+
name
6+
}
7+
}
8+
}
9+
10+
fragment sectionFragment on Home {
11+
sectionA {
12+
id
13+
imageUrl
14+
}
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
type Query {
2+
home: Home!
3+
}
4+
5+
type Home {
6+
sectionA: SectionA
7+
}
8+
9+
type SectionA {
10+
id: String!
11+
name: String!
12+
imageUrl: String
13+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.apollographql.apollo
2+
3+
import com.apollographql.apollo.Utils.immediateExecutor
4+
import com.apollographql.apollo.Utils.immediateExecutorService
5+
import com.apollographql.apollo.Utils.mockResponse
6+
import com.apollographql.apollo.api.Operation
7+
import com.apollographql.apollo.api.ResponseField
8+
import com.apollographql.apollo.cache.normalized.CacheKey
9+
import com.apollographql.apollo.cache.normalized.CacheKeyResolver
10+
import com.apollographql.apollo.cache.normalized.lru.EvictionPolicy
11+
import com.apollographql.apollo.cache.normalized.lru.LruNormalizedCacheFactory
12+
import com.apollographql.apollo.coroutines.await
13+
import com.apollographql.apollo.fetcher.ApolloResponseFetchers
14+
import com.apollographql.apollo.integration.fragmentoverwrites.HomeQuery
15+
import com.apollographql.apollo.integration.fragmentoverwrites.fragment.SectionFragment
16+
import com.google.common.truth.Truth.assertThat
17+
import kotlinx.coroutines.runBlocking
18+
import okhttp3.Dispatcher
19+
import okhttp3.OkHttpClient
20+
import okhttp3.mockwebserver.MockWebServer
21+
import org.junit.Before
22+
import org.junit.Rule
23+
import org.junit.Test
24+
25+
class FragmentOverwritesTest {
26+
27+
private lateinit var apolloClient: ApolloClient
28+
29+
@get:Rule
30+
val server = MockWebServer()
31+
32+
private val cacheKeyResolver = object : CacheKeyResolver() {
33+
override fun fromFieldArguments(field: ResponseField, variables: Operation.Variables): CacheKey {
34+
return CacheKey.NO_KEY
35+
}
36+
37+
override fun fromFieldRecordSet(field: ResponseField, recordSet: Map<String, Any>): CacheKey {
38+
return (recordSet["id"] as? String)?.let { CacheKey.from(it) } ?: CacheKey.NO_KEY
39+
}
40+
}
41+
42+
@Before
43+
fun setup() {
44+
val okHttpClient = OkHttpClient.Builder()
45+
.dispatcher(Dispatcher(immediateExecutorService()))
46+
.build()
47+
48+
apolloClient = ApolloClient.builder()
49+
.normalizedCache(LruNormalizedCacheFactory(EvictionPolicy.NO_EVICTION), cacheKeyResolver)
50+
.okHttpClient(okHttpClient)
51+
.dispatcher(immediateExecutor())
52+
.serverUrl(server.url("/"))
53+
.build()
54+
}
55+
56+
@Test
57+
fun `doesn't overwrite cache entries when using fragments`() {
58+
59+
server.enqueue(mockResponse("FragmentOverwritesTestHomeQueryResponse.json"))
60+
61+
runBlocking {
62+
val networkResponse = apolloClient.query(HomeQuery()).await()
63+
64+
assertThat(networkResponse.data?.home?.sectionA?.name).isEqualTo("initialSectionName")
65+
assertThat(networkResponse.data?.home?.fragments?.sectionFragment?.sectionA?.imageUrl).isEqualTo("initialUrl")
66+
67+
apolloClient.apolloStore.writeAndPublish(
68+
HomeQuery(),
69+
HomeQuery.Data(
70+
HomeQuery.Home(
71+
sectionA = HomeQuery.SectionA(
72+
name = "modifiedSectionName"
73+
),
74+
fragments = HomeQuery.Home.Fragments(
75+
sectionFragment = SectionFragment(
76+
sectionA = SectionFragment.SectionA(
77+
id = "section-id",
78+
imageUrl = "modifiedUrl",
79+
),
80+
)
81+
)
82+
)
83+
)
84+
).execute()
85+
86+
val cacheResponse = apolloClient.query(HomeQuery())
87+
.toBuilder()
88+
.responseFetcher(ApolloResponseFetchers.CACHE_ONLY)
89+
.build()
90+
.await()
91+
92+
assertThat(cacheResponse.data?.home?.sectionA?.name).isEqualTo("modifiedSectionName")
93+
assertThat(cacheResponse.data?.home?.fragments?.sectionFragment?.sectionA?.imageUrl).isEqualTo("modifiedUrl")
94+
}
95+
}
96+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"data": {
3+
"home": {
4+
"__typename": "Home",
5+
"sectionA": {
6+
"__typename": "SectionA",
7+
"name": "initialSectionName",
8+
"id": "section-id",
9+
"imageUrl": "initialUrl"
10+
}
11+
}
12+
}
13+
}

apollo-runtime/src/main/java/com/apollographql/apollo/internal/response/RealResponseWriter.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.apollographql.apollo.internal.response
22

3-
import com.apollographql.apollo.api.CustomTypeAdapter
43
import com.apollographql.apollo.api.Operation
54
import com.apollographql.apollo.api.ResponseField
65
import com.apollographql.apollo.api.ScalarType
@@ -47,7 +46,22 @@ class RealResponseWriter(private val operationVariables: Operation.Variables, pr
4746
}
4847
val nestedResponseWriter = RealResponseWriter(operationVariables, scalarTypeAdapters)
4948
marshaller.marshal(nestedResponseWriter)
50-
buffer[field.responseName] = FieldDescriptor(field, nestedResponseWriter.buffer)
49+
buffer[field.responseName] = deepMergeObjects(field, buffer[field.responseName]?.value, nestedResponseWriter.buffer)
50+
}
51+
52+
private fun deepMergeObjects(field: ResponseField, oldValue: Any?, newValue: Map<String, FieldDescriptor>): FieldDescriptor {
53+
return if (oldValue == null || oldValue !is Map<*, *>) {
54+
FieldDescriptor(field, newValue)
55+
} else {
56+
val oldMap = oldValue as Map<String, FieldDescriptor>
57+
58+
val mergedCommonValues = oldMap.keys.intersect(newValue.keys)
59+
.filter { newValue[it]?.value is Map<*, *> }
60+
.map { deepMergeObjects(oldMap[it]!!.field, oldMap[it]?.value, newValue[it]!!.value as Map<String, FieldDescriptor>) }
61+
.associateBy { it.field.responseName }
62+
63+
FieldDescriptor(field, oldMap + newValue + mergedCommonValues)
64+
}
5165
}
5266

5367
override fun writeFragment(marshaller: ResponseFieldMarshaller?) {
@@ -226,4 +240,4 @@ class RealResponseWriter(private val operationVariables: Operation.Variables, pr
226240
}
227241
}
228242

229-
}
243+
}

0 commit comments

Comments
 (0)