Skip to content

Commit

Permalink
Merge pull request #92 from futuredapp/feature/update-apollo
Browse files Browse the repository at this point in the history
Update apollo to 4.0.0
  • Loading branch information
matejsemancik authored Sep 23, 2024
2 parents da56af7 + 7157a89 commit d293f58
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 77 deletions.
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ktlintGradlePlugin = "12.1.1"
ktlint = "1.2.1"
detektGradlePlugin = "1.23.6"
composeLint = "1.3.1"
apollo = "3.8.4"
apollo = "4.0.0"
ktorfit = "2.1.0" # Must be compatible with: `ksp`
ktorfitConverter = "2.1.0"
ktor = "2.3.12" # Must be compatible with: `ktorfit`
Expand Down Expand Up @@ -116,9 +116,9 @@ skie-annotations = { group = "co.touchlab.skie", name = "configuration-annotatio
moko-resources = { group = "dev.icerock.moko", name = "resources", version.ref = "moko-resources" }

# Network
network-apollo-runtime = { group = "com.apollographql.apollo3", name = "apollo-runtime", version.ref = "apollo" }
network-apollo-normalizedCache-core = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache", version.ref = "apollo" }
network-apollo-normalizedCache-sqlite = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache-sqlite", version.ref = "apollo" }
network-apollo-runtime = { group = "com.apollographql.apollo", name = "apollo-runtime", version.ref = "apollo" }
network-apollo-normalizedCache-core = { group = "com.apollographql.apollo", name = "apollo-normalized-cache", version.ref = "apollo" }
network-apollo-normalizedCache-sqlite = { group = "com.apollographql.apollo", name = "apollo-normalized-cache-sqlite", version.ref = "apollo" }
network-ktorfit-ksp = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-ksp", version.ref = "ktorfit" }
network-ktorfit-lib = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-lib", version.ref = "ktorfit" }
network-ktorfit-converters = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-converters-response", version.ref = "ktorfitConverter" }
Expand Down Expand Up @@ -148,7 +148,7 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
apollo = { id = "com.apollographql.apollo3", version.ref = "apollo" }
apollo = { id = "com.apollographql.apollo", version.ref = "apollo" }
ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" }
skie = { id = "co.touchlab.skie", version.ref = "skie" }
buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app.futured.kmptemplate.network.graphql.cache

import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.cache.normalized.apolloStore
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.cache.normalized.apolloStore
import org.koin.core.annotation.Single

@Single
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package app.futured.kmptemplate.network.graphql.cache

import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory
import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory
import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory
import com.apollographql.apollo.cache.normalized.api.MemoryCacheFactory
import com.apollographql.apollo.cache.normalized.api.NormalizedCacheFactory
import com.apollographql.apollo.cache.normalized.sql.SqlNormalizedCacheFactory
import org.koin.core.annotation.Single

@Single
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package app.futured.kmptemplate.network.graphql.cache

import app.futured.kmptemplate.network.graphql.cache.NormalizedCacheKeyGenerator.Companion.KNOWN_TYPES
import co.touchlab.kermit.Logger
import com.apollographql.apollo3.cache.normalized.api.CacheKey
import com.apollographql.apollo3.cache.normalized.api.CacheKeyGenerator
import com.apollographql.apollo3.cache.normalized.api.CacheKeyGeneratorContext
import com.apollographql.apollo3.cache.normalized.api.Record
import com.apollographql.apollo.cache.normalized.api.CacheKey
import com.apollographql.apollo.cache.normalized.api.CacheKeyGenerator
import com.apollographql.apollo.cache.normalized.api.CacheKeyGeneratorContext
import com.apollographql.apollo.cache.normalized.api.Record
import org.koin.core.annotation.Single

private typealias Typename = String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import app.futured.kmptemplate.network.graphql.ext.isUnauthorizedCloudError
import app.futured.kmptemplate.network.graphql.result.CloudErrorCode
import app.futured.kmptemplate.network.graphql.result.NetworkError
import app.futured.kmptemplate.network.graphql.result.NetworkResult
import com.apollographql.apollo3.api.Mutation
import com.apollographql.apollo3.api.Query
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Query
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
Expand All @@ -25,41 +25,44 @@ internal interface ApiManager {
*
* @param query to be executed
* @param fetchPolicy fetch policy to apply to this query
* @param throwOnPartialData whether to throw an exception on partial data
* @return the result of the [query] wrapped with [NetworkResult]
*/
suspend fun <DATA : Query.Data> executeQuery(
query: Query<DATA>,
fetchPolicy: FetchPolicy = FetchPolicy.NetworkOnly,
throwOnPartialData: Boolean = false,
): NetworkResult<DATA> =
runWrapping { errorInterceptor { apiAdapter.executeQuery(query, fetchPolicy) } }
runWrapping { errorInterceptor { apiAdapter.executeQuery(query, fetchPolicy, throwOnPartialData) } }

/**
* Executes the [Query] and then watches it from normalized cache.
* Returns Flow of [DATA] wrapped with [NetworkResult].
*
* @param query to be executed and watched.
* @param fetchPolicy fetch policy to apply to initial fetch.
* @param fetchThrows whether to emit [NetworkResult.Failure]s during the initial fetch.
* @param refetchThrows whether to emit [NetworkResult.Failure]s during a refetch.
* @param filterOutExceptions whether to filter out exceptions from the flow.
* @param throwOnPartialData whether to throw an exception on partial data
* @return Flow of [DATA] wrapped in [NetworkResult]
*/
fun <DATA : Query.Data> executeQueryWatcher(
query: Query<DATA>,
fetchPolicy: FetchPolicy,
fetchThrows: Boolean = true,
refetchThrows: Boolean = false,
filterOutExceptions: Boolean = false,
throwOnPartialData: Boolean = false,
): Flow<NetworkResult<DATA>> = apiAdapter
.watchQueryWatcher(query, fetchPolicy, fetchThrows, refetchThrows)
.watchQueryWatcher(query, fetchPolicy, filterOutExceptions, throwOnPartialData)
.map { result -> runWrapping { errorInterceptor { result.getOrThrow() } } }

/**
* Executes the [Mutation] and returns the [DATA] response wrapped with [NetworkResult].
*
* @param mutation to be executed
* @param throwOnPartialData whether to throw an exception on partial data
* @return the result of the [mutation] wrapped with [NetworkResult]
*/
suspend fun <DATA : Mutation.Data> executeMutation(mutation: Mutation<DATA>): NetworkResult<DATA> =
runWrapping { errorInterceptor { apiAdapter.executeMutation(mutation) } }
suspend fun <DATA : Mutation.Data> executeMutation(mutation: Mutation<DATA>, throwOnPartialData: Boolean = false): NetworkResult<DATA> =
runWrapping { errorInterceptor { apiAdapter.executeMutation(mutation, throwOnPartialData) } }

/**
* Executes [operation], wrapping result in [NetworkResult].
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package app.futured.kmptemplate.network.graphql.client

import app.futured.kmptemplate.network.graphql.result.NetworkError
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.Mutation
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.api.Query
import com.apollographql.apollo3.cache.normalized.fetchPolicy
import com.apollographql.apollo3.cache.normalized.watch
import com.apollographql.apollo3.exception.ApolloException
import com.apollographql.apollo3.exception.ApolloHttpException
import com.apollographql.apollo3.exception.ApolloNetworkException
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.Query
import com.apollographql.apollo.cache.normalized.fetchPolicy
import com.apollographql.apollo.cache.normalized.watch
import com.apollographql.apollo.exception.ApolloHttpException
import com.apollographql.apollo.exception.ApolloNetworkException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single

Expand All @@ -29,47 +29,51 @@ internal class ApolloApiAdapter(
*
* @param query to be executed
* @param fetchPolicy fetch policy to apply
* @param throwOnPartialData whether to throw an exception on partial data
* @return [DATA] as a result of the [query]
*/
suspend fun <DATA : Query.Data> executeQuery(query: Query<DATA>, fetchPolicy: FetchPolicy): DATA =
executeOperation { apolloClient.query(query).fetchPolicy(fetchPolicy.asApolloFetchPolicy()).execute() }
suspend fun <DATA : Query.Data> executeQuery(query: Query<DATA>, fetchPolicy: FetchPolicy, throwOnPartialData: Boolean): DATA =
executeOperation(throwOnPartialData) { apolloClient.query(query).fetchPolicy(fetchPolicy.asApolloFetchPolicy()).execute() }

/**
* Executes the [Query] and then watches it from normalized cache.
*
* @param query to be executed and watched.
* @param filterOutExceptions whether to filter out exceptions from the flow.
* @param throwOnPartialData whether to throw an exception on partial data
* @param fetchPolicy fetch policy to apply to initial fetch.
* @param fetchThrows whether to throw if an [ApolloException] happens during the initial fetch.
* @param refetchThrows whether to throw if an [ApolloException] happens during a refetch.
* @return Flow of [DATA] wrapped in Kotlin result
*/
fun <DATA : Query.Data> watchQueryWatcher(
query: Query<DATA>,
fetchPolicy: FetchPolicy,
fetchThrows: Boolean = true,
refetchThrows: Boolean = false,
filterOutExceptions: Boolean,
throwOnPartialData: Boolean,
): Flow<Result<DATA>> =
apolloClient
.query(query)
.fetchPolicy(fetchPolicy.asApolloFetchPolicy())
.watch(fetchThrows = fetchThrows, refetchThrows = refetchThrows)
.map { apolloResponse -> runCatching { executeOperation { apolloResponse } } }
.watch()
.filter { if (filterOutExceptions) it.exception == null else true }
.map { apolloResponse -> runCatching { executeOperation(throwOnPartialData) { apolloResponse } } }


/**
* Executes the [Mutation] and returns the [DATA].
*
* @param mutation to be executed
* @param throwOnPartialData whether to throw an exception on partial data
* @return [DATA] as a result of the [mutation]
*/
suspend fun <DATA : Mutation.Data> executeMutation(mutation: Mutation<DATA>): DATA =
executeOperation { apolloClient.mutation(mutation).execute() }
suspend fun <DATA : Mutation.Data> executeMutation(mutation: Mutation<DATA>, throwOnPartialData: Boolean): DATA =
executeOperation(throwOnPartialData) { apolloClient.mutation(mutation).execute() }

private suspend fun <DATA : Operation.Data> executeOperation(
throwOnPartialData: Boolean,
apolloCall: suspend () -> ApolloResponse<DATA>,
): DATA {
val result = runCatching {
unfoldResult(apolloCall())
unfoldResult(apolloCall(), throwOnPartialData)
}

result.fold(
Expand All @@ -95,16 +99,41 @@ internal class ApolloApiAdapter(
}

/**
* Unwraps successful [ApolloResponse] either into [DATA], or throws [NetworkError.CloudError]
* in case errors are present in the response.
* https://www.apollographql.com/docs/kotlin/essentials/errors#truth-table
* Unwraps successful [ApolloResponse] either into [DATA], or throws [NetworkError].
* If the response contains an exception, it throws the exception.
* If the response contains complete data, it returns the data.
* If the response contains partial data, it returns the data or throws an exception (It's possible to have both
* [ApolloResponse.data] non-null and [ApolloResponse.errors] non-null.)
* If the response contains errors, it throws [NetworkError.CloudError].
* @param throwOnPartialData whether to throw an exception on partial data
*/
private fun <DATA : Operation.Data> unfoldResult(response: ApolloResponse<DATA>): DATA =
if (response.hasErrors().not()) {
response.data ?: error("Response without errors and with no data")
} else {
throw NetworkError.CloudError(
code = errorResponseParser.getErrorResponseCode(response),
message = errorResponseParser.getErrorMessage(response) ?: "",
)
private fun <DATA : Operation.Data> unfoldResult(
response: ApolloResponse<DATA>,
throwOnPartialData: Boolean,
): DATA {
response.exception?.let { throw it }

response.data?.let { data ->
if (response.hasErrors()) {
if (throwOnPartialData) {
throw NetworkError.CloudError(
code = errorResponseParser.getErrorResponseCode(response),
message = errorResponseParser.getErrorMessage(response),
)
}
// Return partial data
return data
}
// Return complete data
return data
}

// Non-compliant server, if data is null, there should be at least one error, or
// A GraphQL request error happened or a Graph field error bubbled up.
throw NetworkError.CloudError(
code = errorResponseParser.getErrorResponseCode(response),
message = errorResponseParser.getErrorMessage(response)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import app.futured.kmptemplate.network.graphql.cache.NetworkNormalizedCacheFacto
import app.futured.kmptemplate.network.graphql.cache.NormalizedCacheKeyGenerator
import app.futured.kmptemplate.network.graphql.interceptor.LoggingInterceptorFactory
import app.futured.kmptemplate.network.graphql.tools.Constants
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.cache.normalized.FetchPolicy
import com.apollographql.apollo3.cache.normalized.fetchPolicy
import com.apollographql.apollo3.cache.normalized.normalizedCache
import com.apollographql.apollo3.network.http.DefaultHttpEngine
import com.apollographql.apollo3.network.http.HttpNetworkTransport
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.cache.normalized.FetchPolicy
import com.apollographql.apollo.cache.normalized.fetchPolicy
import com.apollographql.apollo.cache.normalized.normalizedCache
import com.apollographql.apollo.network.http.DefaultHttpEngine
import com.apollographql.apollo.network.http.HttpNetworkTransport
import org.koin.core.annotation.Single

@Single
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app.futured.kmptemplate.network.graphql.client

import app.futured.kmptemplate.network.graphql.result.CloudErrorCode
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo.api.ApolloResponse
import org.koin.core.annotation.Single

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package app.futured.kmptemplate.network.graphql.client

import com.apollographql.apollo3.exception.ApolloCompositeException
import com.apollographql.apollo3.exception.ApolloException
import com.apollographql.apollo3.exception.CacheMissException
import com.apollographql.apollo3.cache.normalized.FetchPolicy as ApolloFetchPolicy
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.exception.ApolloException
import com.apollographql.apollo.cache.normalized.FetchPolicy as ApolloFetchPolicy

enum class FetchPolicy {
/**
* Try the cache, if that failed, try the network.
*
* An [ApolloCompositeException] is thrown if the data is not in the cache and the network call failed.
* If coming from the cache 1 value is emitted, otherwise 1 or multiple values can be emitted from the network.
* This [FetchPolicy] emits one or more [ApolloResponse].
* Cache misses and network errors have [ApolloResponse.exception] set to a non-null [ApolloException].
*
* This is the default behaviour.
*/
Expand All @@ -19,31 +18,33 @@ enum class FetchPolicy {
/**
* Only try the cache.
*
* A [CacheMissException] is thrown if the data is not in the cache, otherwise 1 value is emitted.
* This [FetchPolicy] emits one [ApolloResponse].
* Cache misses have [ApolloResponse.exception] set to a non-null [ApolloException].
*/
CacheOnly,

/**
* Try the network, if that failed, try the cache.
*
* An [ApolloCompositeException] is thrown if the network call failed and the data is not in the cache.
* If coming from the network 1 or multiple values can be emitted, otherwise 1 value is emitted from the cache.
* This [FetchPolicy] emits one or more [ApolloResponse].
* Cache misses and network errors have [ApolloResponse.exception] set to a non-null [ApolloException].
*/
NetworkFirst,

/**
* Only try the network.
*
* An [ApolloException] is thrown if the network call failed, otherwise 1 or multiple values can be emitted.
* This FetchPolicy emits one or more [ApolloResponse].
* Several [ApolloResponse] may be emitted if your NetworkTransport supports it, for example with @defer.
* Network errors have [ApolloResponse.exception] set to a non-null[ApolloException].
*/
NetworkOnly,

/**
* Try the cache, then also try the network.
*
* If the data is in the cache, it is emitted, if not, no exception is thrown at that point. Then the network call is made, and if
* successful the value(s) are emitted, otherwise either an [ApolloCompositeException] (both cache miss and network failed) or an
* [ApolloException] (only network failed) is thrown.
* This FetchPolicy emits two or more [ApolloResponse].
* Cache misses and network errors have [ApolloResponse.exception] set to a non-null [ApolloException].
*/
CacheAndNetwork,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app.futured.kmptemplate.network.graphql.injection

import app.futured.kmptemplate.network.graphql.client.ApolloClientFactory
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo.ApolloClient
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
import org.koin.core.annotation.Single
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package app.futured.kmptemplate.network.graphql.interceptor

import co.touchlab.kermit.Logger
import com.apollographql.apollo3.network.http.LoggingInterceptor
import com.apollographql.apollo.network.http.LoggingInterceptor
import org.koin.core.annotation.Single

@Single
Expand Down

0 comments on commit d293f58

Please sign in to comment.