diff --git a/CHANGELOG.md b/CHANGELOG.md index df158d3afcc..8da2d1a3c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Change Log # Next version * Downloading or converting a SDL schema from introspection now includes scalar definitions. This is required for clients to get a [full view of the schema](https://github.com/graphql/graphql-wg/blob/main/rfcs/FullSchemas.md). +* The cache and auto persisted queries interceptors are now always added after all users interceptor. If you relied on some interceptors being called **after** `normalizedCache()` or `persistedQueries()`, you might have to update your code. One example is if you need to change cache flags based on the GraphQL response. In those cases, we recommend you use a custom `NetworkTransport` instead (See [this commit](https://github.com/apollographql/apollo-kotlin/pull/6455/commits/a53a44e5e506af7b0f6f495eed1a9d477e18bf73) for an example). # Version 4.1.1 diff --git a/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/ClientCacheExtensions.kt b/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/ClientCacheExtensions.kt index e622490371d..6d151ae35d4 100644 --- a/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/ClientCacheExtensions.kt +++ b/libraries/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/ClientCacheExtensions.kt @@ -113,26 +113,54 @@ fun ApolloClient.Builder.normalizedCache( fun ApolloClient.Builder.logCacheMisses( log: (String) -> Unit = { println(it) }, ): ApolloClient.Builder { - check(interceptors.none { it is ApolloCacheInterceptor }) { - "Apollo: logCacheMisses() must be called before setting up your normalized cache" - } return addInterceptor(CacheMissLoggingInterceptor(log)) } -fun ApolloClient.Builder.store(store: ApolloStore, writeToCacheAsynchronously: Boolean = false): ApolloClient.Builder { - check(interceptors.none { it is AutoPersistedQueryInterceptor }) { - "Apollo: the normalized cache must be configured before the auto persisted queries" +private class DefaultInterceptorChain( + private val interceptors: List, + private val index: Int, +) : ApolloInterceptorChain { + + override fun proceed(request: ApolloRequest): Flow> { + check(index < interceptors.size) + return interceptors[index].intercept( + request, + DefaultInterceptorChain( + interceptors = interceptors, + index = index + 1, + ) + ) + } +} + +private fun ApolloInterceptorChain.asInterceptor(): ApolloInterceptor { + return object : ApolloInterceptor { + override fun intercept( + request: ApolloRequest, + chain: ApolloInterceptorChain, + ): Flow> { + return this@asInterceptor.proceed(request) + } } - // Removing existing interceptors added for configuring an [ApolloStore]. - // If a builder is reused from an existing client using `newBuilder()` and we try to configure a new `store()` on it, we first need to - // remove the old interceptors. - val storeInterceptors = interceptors.filterIsInstance() - storeInterceptors.forEach { - removeInterceptor(it) +} +internal class CacheInterceptor(val store: ApolloStore): ApolloInterceptor { + private val delegates = listOf( + WatcherInterceptor(store), + FetchPolicyRouterInterceptor, + ApolloCacheInterceptor(store) + ) + + override fun intercept( + request: ApolloRequest, + chain: ApolloInterceptorChain, + ): Flow> { + return DefaultInterceptorChain(delegates + chain.asInterceptor(), 0).proceed(request) } - return addInterceptor(WatcherInterceptor(store)) - .addInterceptor(FetchPolicyRouterInterceptor) - .addInterceptor(ApolloCacheInterceptor(store)) +} + + +fun ApolloClient.Builder.store(store: ApolloStore, writeToCacheAsynchronously: Boolean = false): ApolloClient.Builder { + return cacheInterceptor(CacheInterceptor(store)) .writeToCacheAsynchronously(writeToCacheAsynchronously) .addExecutionContext(CacheDumpProviderContext(store.cacheDumpProvider())) } @@ -228,9 +256,7 @@ internal fun ApolloCall.watchInternal(data: D?): Flow(): com.apollographql.apollo.interceptor/ApolloInterceptor? // com.apollographql.apollo/ApolloClient.cacheInterceptor.|(){}[0] final val canBeBatched // com.apollographql.apollo/ApolloClient.canBeBatched|{}canBeBatched[0] final fun (): kotlin/Boolean? // com.apollographql.apollo/ApolloClient.canBeBatched.|(){}[0] final val customScalarAdapters // com.apollographql.apollo/ApolloClient.customScalarAdapters|{}customScalarAdapters[0] @@ -627,6 +629,10 @@ final class com.apollographql.apollo/ApolloClient : com.apollographql.apollo.api final val interceptors // com.apollographql.apollo/ApolloClient.Builder.interceptors|{}interceptors[0] final fun (): kotlin.collections/List // com.apollographql.apollo/ApolloClient.Builder.interceptors.|(){}[0] + final var autoPersistedQueryInterceptor // com.apollographql.apollo/ApolloClient.Builder.autoPersistedQueryInterceptor|{}autoPersistedQueryInterceptor[0] + final fun (): com.apollographql.apollo.interceptor/ApolloInterceptor? // com.apollographql.apollo/ApolloClient.Builder.autoPersistedQueryInterceptor.|(){}[0] + final var cacheInterceptor // com.apollographql.apollo/ApolloClient.Builder.cacheInterceptor|{}cacheInterceptor[0] + final fun (): com.apollographql.apollo.interceptor/ApolloInterceptor? // com.apollographql.apollo/ApolloClient.Builder.cacheInterceptor.|(){}[0] final var canBeBatched // com.apollographql.apollo/ApolloClient.Builder.canBeBatched|{}canBeBatched[0] final fun (): kotlin/Boolean? // com.apollographql.apollo/ApolloClient.Builder.canBeBatched.|(){}[0] final var dispatcher // com.apollographql.apollo/ApolloClient.Builder.dispatcher|{}dispatcher[0] @@ -680,7 +686,9 @@ final class com.apollographql.apollo/ApolloClient : com.apollographql.apollo.api final fun addInterceptors(kotlin.collections/List): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.addInterceptors|addInterceptors(kotlin.collections.List){}[0] final fun addListener(com.apollographql.apollo.internal/ApolloClientListener): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.addListener|addListener(com.apollographql.apollo.internal.ApolloClientListener){}[0] final fun autoPersistedQueries(com.apollographql.apollo.api.http/HttpMethod = ..., com.apollographql.apollo.api.http/HttpMethod = ..., kotlin/Boolean = ...): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.autoPersistedQueries|autoPersistedQueries(com.apollographql.apollo.api.http.HttpMethod;com.apollographql.apollo.api.http.HttpMethod;kotlin.Boolean){}[0] + final fun autoPersistedQueriesInterceptor(com.apollographql.apollo.interceptor/ApolloInterceptor?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.autoPersistedQueriesInterceptor|autoPersistedQueriesInterceptor(com.apollographql.apollo.interceptor.ApolloInterceptor?){}[0] final fun build(): com.apollographql.apollo/ApolloClient // com.apollographql.apollo/ApolloClient.Builder.build|build(){}[0] + final fun cacheInterceptor(com.apollographql.apollo.interceptor/ApolloInterceptor?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.cacheInterceptor|cacheInterceptor(com.apollographql.apollo.interceptor.ApolloInterceptor?){}[0] final fun canBeBatched(kotlin/Boolean?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.canBeBatched|canBeBatched(kotlin.Boolean?){}[0] final fun copy(): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.copy|copy(){}[0] final fun customScalarAdapters(com.apollographql.apollo.api/CustomScalarAdapters): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.customScalarAdapters|customScalarAdapters(com.apollographql.apollo.api.CustomScalarAdapters){}[0] diff --git a/libraries/apollo-runtime/api/jvm/apollo-runtime.api b/libraries/apollo-runtime/api/jvm/apollo-runtime.api index 0f27cbc7a49..1603f3e27ba 100644 --- a/libraries/apollo-runtime/api/jvm/apollo-runtime.api +++ b/libraries/apollo-runtime/api/jvm/apollo-runtime.api @@ -43,6 +43,7 @@ public final class com/apollographql/apollo/ApolloClient : com/apollographql/apo public fun close ()V public final fun dispose ()V public final fun executeAsFlow (Lcom/apollographql/apollo/api/ApolloRequest;)Lkotlinx/coroutines/flow/Flow; + public final fun getCacheInterceptor ()Lcom/apollographql/apollo/interceptor/ApolloInterceptor; public fun getCanBeBatched ()Ljava/lang/Boolean; public final fun getCustomScalarAdapters ()Lcom/apollographql/apollo/api/CustomScalarAdapters; public fun getEnableAutoPersistedQueries ()Ljava/lang/Boolean; @@ -76,7 +77,9 @@ public final class com/apollographql/apollo/ApolloClient$Builder : com/apollogra public final fun autoPersistedQueries (Lcom/apollographql/apollo/api/http/HttpMethod;Lcom/apollographql/apollo/api/http/HttpMethod;)Lcom/apollographql/apollo/ApolloClient$Builder; public final fun autoPersistedQueries (Lcom/apollographql/apollo/api/http/HttpMethod;Lcom/apollographql/apollo/api/http/HttpMethod;Z)Lcom/apollographql/apollo/ApolloClient$Builder; public static synthetic fun autoPersistedQueries$default (Lcom/apollographql/apollo/ApolloClient$Builder;Lcom/apollographql/apollo/api/http/HttpMethod;Lcom/apollographql/apollo/api/http/HttpMethod;ZILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder; + public final fun autoPersistedQueriesInterceptor (Lcom/apollographql/apollo/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder; public final fun build ()Lcom/apollographql/apollo/ApolloClient; + public final fun cacheInterceptor (Lcom/apollographql/apollo/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder; public fun canBeBatched (Ljava/lang/Boolean;)Lcom/apollographql/apollo/ApolloClient$Builder; public synthetic fun canBeBatched (Ljava/lang/Boolean;)Ljava/lang/Object; public final fun copy ()Lcom/apollographql/apollo/ApolloClient$Builder; @@ -86,6 +89,8 @@ public final class com/apollographql/apollo/ApolloClient$Builder : com/apollogra public synthetic fun enableAutoPersistedQueries (Ljava/lang/Boolean;)Ljava/lang/Object; public final fun executionContext (Lcom/apollographql/apollo/api/ExecutionContext;)Lcom/apollographql/apollo/ApolloClient$Builder; public final fun failFastIfOffline (Ljava/lang/Boolean;)Lcom/apollographql/apollo/ApolloClient$Builder; + public final fun getAutoPersistedQueryInterceptor ()Lcom/apollographql/apollo/interceptor/ApolloInterceptor; + public final fun getCacheInterceptor ()Lcom/apollographql/apollo/interceptor/ApolloInterceptor; public fun getCanBeBatched ()Ljava/lang/Boolean; public final fun getCustomScalarAdapters ()Lcom/apollographql/apollo/api/CustomScalarAdapters; public final fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/ApolloClient.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/ApolloClient.kt index 1f6dc4e9c3c..b8fa916e3a9 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/ApolloClient.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/ApolloClient.kt @@ -82,6 +82,8 @@ private constructor( val subscriptionNetworkTransport: NetworkTransport val interceptors: List = builder.interceptors val customScalarAdapters: CustomScalarAdapters = builder.customScalarAdapters + val cacheInterceptor: ApolloInterceptor? = builder.cacheInterceptor + private val autoPersistedQueryInterceptor: ApolloInterceptor? = builder.autoPersistedQueryInterceptor private val retryOnError: ((ApolloRequest<*>) -> Boolean)? = builder.retryOnError private val retryOnErrorInterceptor: ApolloInterceptor? = builder.retryOnErrorInterceptor private val failFastIfOffline = builder.failFastIfOffline @@ -315,6 +317,12 @@ private constructor( val allInterceptors = buildList { addAll(interceptors) + if (cacheInterceptor != null) { + add(cacheInterceptor) + } + if (autoPersistedQueryInterceptor != null) { + add(autoPersistedQueryInterceptor) + } add(retryOnErrorInterceptor ?: RetryOnErrorInterceptor()) add(networkInterceptor) } @@ -407,6 +415,12 @@ private constructor( var failFastIfOffline: Boolean? = null private set + var cacheInterceptor: ApolloInterceptor? = null + private set + + var autoPersistedQueryInterceptor: ApolloInterceptor? = null + private set + /** * Whether to fail fast if the device is offline. * Requires setting an interceptor that is aware of the network state with [retryOnErrorInterceptor]. @@ -466,6 +480,24 @@ private constructor( this.retryOnErrorInterceptor = retryOnErrorInterceptor } + /** + * Sets the [ApolloInterceptor] used for caching. + * + * @see addInterceptor + */ + fun cacheInterceptor(cacheInterceptor: ApolloInterceptor?) = apply { + this.cacheInterceptor = cacheInterceptor + } + + /** + * Sets the [ApolloInterceptor] used for auto persisted queries. + * + * @see addInterceptor + */ + fun autoPersistedQueriesInterceptor(autoPersistedQueryInterceptor: ApolloInterceptor?) = apply { + this.autoPersistedQueryInterceptor = autoPersistedQueryInterceptor + } + /** * Configures the [HttpMethod] to use. * @@ -791,8 +823,18 @@ private constructor( * such as normalized cache and auto persisted queries. [ApolloClient] also inserts a terminating [ApolloInterceptor] that * executes the request. * - * **The order is important**. The [ApolloInterceptor]s are executed in the order they are added. Because cache and APQs also - * use interceptors, the order of the cache/APQs configuration also influences the final interceptor list. + * **The order is important**. The [ApolloInterceptor]s are added in the order they are added and always added before + * the built-in intercepted: + * + * - user interceptors + * - cacheInterceptor + * - autoPersistedQueriesInterceptor + * - retryOnErrorInterceptor + * - networkInterceptor + * + * @see cacheInterceptor + * @see autoPersistedQueriesInterceptor + * @see retryOnErrorInterceptor */ fun addInterceptor(interceptor: ApolloInterceptor) = apply { _interceptors.add(interceptor) @@ -878,8 +920,7 @@ private constructor( httpMethodForDocumentQueries: HttpMethod = HttpMethod.Post, enableByDefault: Boolean = true, ) = apply { - _interceptors.removeAll { it is AutoPersistedQueryInterceptor } - addInterceptor( + autoPersistedQueriesInterceptor( AutoPersistedQueryInterceptor( httpMethodForHashedQueries, httpMethodForDocumentQueries @@ -915,9 +956,9 @@ private constructor( * Creates an [ApolloClient] from this [Builder] */ fun build(): ApolloClient { - return ApolloClient( - this.copy() - ) + // Copy the builder so that any subsequent modifications of the builder + // doesn't change the ApolloClient owned one + return ApolloClient(copy()) } fun copy(): Builder { @@ -946,6 +987,8 @@ private constructor( .wsProtocol(wsProtocolFactory) .retryOnError(retryOnError) .retryOnErrorInterceptor(retryOnErrorInterceptor) + .cacheInterceptor(cacheInterceptor) + .autoPersistedQueriesInterceptor(autoPersistedQueryInterceptor) .failFastIfOffline(failFastIfOffline) .listeners(listeners) } diff --git a/tests/http-headers/src/test/kotlin/HttpHeaderTest.kt b/tests/http-headers/src/test/kotlin/HttpHeaderTest.kt index 43abc7a7bf2..25830db2c41 100644 --- a/tests/http-headers/src/test/kotlin/HttpHeaderTest.kt +++ b/tests/http-headers/src/test/kotlin/HttpHeaderTest.kt @@ -1,4 +1,3 @@ - import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.DefaultUpload import com.apollographql.apollo.api.Optional @@ -28,7 +27,7 @@ class HttpHeadersTest { @Test fun getRequestsSendPreflightHeader() = mockServerTest( clientBuilder = { autoPersistedQueries() } - ){ + ) { mockServer.enqueueString("") apolloClient.query(GetRandomQuery()).enableAutoPersistedQueries(true).execute() @@ -63,14 +62,14 @@ class MockServerTest(val mockServer: MockServer, val apolloClient: ApolloClient, fun mockServerTest( clientBuilder: ApolloClient.Builder.() -> Unit = {}, - block: suspend MockServerTest.() -> Unit + block: suspend MockServerTest.() -> Unit, ) = runTest { MockServer().use { mockServer -> ApolloClient.Builder() .serverUrl(mockServer.url()) .apply(clientBuilder) .build() - .use {apolloClient -> + .use { apolloClient -> MockServerTest(mockServer, apolloClient, this).block() } } diff --git a/tests/integration-tests/src/commonTest/kotlin/test/CacheFlagsTest.kt b/tests/integration-tests/src/commonTest/kotlin/test/CacheFlagsTest.kt index 99aa9147976..5934b631f83 100644 --- a/tests/integration-tests/src/commonTest/kotlin/test/CacheFlagsTest.kt +++ b/tests/integration-tests/src/commonTest/kotlin/test/CacheFlagsTest.kt @@ -19,9 +19,13 @@ import com.apollographql.apollo.exception.CacheMissException import com.apollographql.apollo.integration.normalizer.HeroNameQuery import com.apollographql.apollo.interceptor.ApolloInterceptor import com.apollographql.apollo.interceptor.ApolloInterceptorChain +import com.apollographql.apollo.network.NetworkMonitor +import com.apollographql.apollo.network.NetworkTransport +import com.apollographql.apollo.network.http.HttpNetworkTransport import com.apollographql.apollo.testing.QueueTestNetworkTransport import com.apollographql.apollo.testing.enqueueTestResponse import com.apollographql.apollo.testing.internal.runTest +import com.benasher44.uuid.uuid4 import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlin.test.Test @@ -109,17 +113,29 @@ class CacheFlagsTest { } @Test - fun doNotStoreWhenSetInResponse() = runTest(before = { setUp() }) { + fun doNotStoreWhenSetInResponse() = runTest { val query = HeroNameQuery() val data = HeroNameQuery.Data(HeroNameQuery.Hero("R2-D2")) - apolloClient = apolloClient.newBuilder().addInterceptor(object: ApolloInterceptor{ - override fun intercept(request: ApolloRequest, chain: ApolloInterceptorChain): Flow> { - return chain.proceed(request).map { response -> - response.newBuilder().cacheHeaders(CacheHeaders.Builder().addHeader(ApolloCacheHeaders.DO_NOT_STORE, "").build()).build() - } - } - }).build() - apolloClient.enqueueTestResponse(query, data) + + store = ApolloStore(MemoryCacheFactory()) + val queueTestNetworkTransport = QueueTestNetworkTransport() + apolloClient = ApolloClient.Builder() + .store(store) + .networkTransport(object : NetworkTransport { + val delegate = queueTestNetworkTransport + override fun execute(request: ApolloRequest): Flow> { + return delegate.execute(request).map { response -> + // Parse data, errors or anything else and decide whether to store the response or not + response.newBuilder().cacheHeaders(CacheHeaders.Builder().addHeader(ApolloCacheHeaders.DO_NOT_STORE, "").build()).build() + } + } + + override fun dispose() { + } + + }) + .build() + queueTestNetworkTransport.enqueue(ApolloResponse.Builder(query, uuid4()).data(data).build()) apolloClient.query(query).fetchPolicy(FetchPolicy.NetworkFirst).execute() diff --git a/tests/integration-tests/src/jvmTest/kotlin/test/ApqCacheTest.kt b/tests/integration-tests/src/jvmTest/kotlin/test/ApqCacheTest.kt deleted file mode 100644 index 10c6a88eedb..00000000000 --- a/tests/integration-tests/src/jvmTest/kotlin/test/ApqCacheTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package test - -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.api.composeJsonResponse -import com.apollographql.apollo.api.http.HttpMethod -import com.apollographql.apollo.cache.normalized.api.MemoryCacheFactory -import com.apollographql.apollo.cache.normalized.normalizedCache -import com.apollographql.apollo.integration.normalizer.HeroNameQuery -import com.apollographql.mockserver.MockServer -import com.apollographql.mockserver.enqueueString -import com.apollographql.apollo.testing.internal.runTest -import org.junit.Test -import kotlin.test.fail - -class ApqCacheTest { - /** - * https://github.com/apollographql/apollo-kotlin/issues/4617 - */ - @Test - fun apqAndCache() = runTest { - val mockServer = MockServer() - - val data = HeroNameQuery.Data(HeroNameQuery.Hero("R2-D2")) - val query = HeroNameQuery() - - mockServer.enqueueString(query.composeJsonResponse(data)) - mockServer.enqueueString(query.composeJsonResponse(data)) - - try { - ApolloClient.Builder() - .serverUrl(mockServer.url()) - // Note that mutations will always be sent as POST requests, regardless of these settings, as to avoid hitting caches. - .autoPersistedQueries( - // For the initial hashed query that does not send the actual Graphql document - httpMethodForHashedQueries = HttpMethod.Get, - // For the follow-up query that sends the full document if the initial hashed query was not found - httpMethodForDocumentQueries = HttpMethod.Get - ) - .normalizedCache(normalizedCacheFactory = MemoryCacheFactory(10 * 1024 * 1024)) - .build() - fail("An exception was expected") - } catch (e: Exception) { - check(e.message!!.contains("Apollo: the normalized cache must be configured before the auto persisted queries")) - } - } -} diff --git a/tests/integration-tests/src/jvmTest/kotlin/test/CacheMissLoggingInterceptorTest.kt b/tests/integration-tests/src/jvmTest/kotlin/test/CacheMissLoggingInterceptorTest.kt index 4b07d22a2a9..1a039c9d9a4 100644 --- a/tests/integration-tests/src/jvmTest/kotlin/test/CacheMissLoggingInterceptorTest.kt +++ b/tests/integration-tests/src/jvmTest/kotlin/test/CacheMissLoggingInterceptorTest.kt @@ -60,17 +60,4 @@ class CacheMissLoggingInterceptorTest { mockServer.close() apolloClient.close() } - - @Test - fun logCacheMissesMustBeCalledFirst() { - try { - ApolloClient.Builder() - .normalizedCache(MemoryCacheFactory()) - .logCacheMisses() - .build() - error("We expected an exception") - } catch (e: Exception) { - assertTrue(e.message?.contains("logCacheMisses() must be called before setting up your normalized cache") == true) - } - } }