All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.apollographql.apollo.ApolloCall.kt Maven / Gradle / Ivy

package com.apollographql.apollo

import com.apollographql.apollo.annotations.ApolloDeprecatedSince
import com.apollographql.apollo.annotations.ApolloExperimental
import com.apollographql.apollo.api.ApolloRequest
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.ExecutionContext
import com.apollographql.apollo.api.MutableExecutionOptions
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.http.HttpHeader
import com.apollographql.apollo.api.http.HttpMethod
import com.apollographql.apollo.exception.ApolloException
import com.apollographql.apollo.exception.DefaultApolloException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.toList

/**
 * An [ApolloCall] is a thin class that binds an [ApolloRequest] with its [ApolloClient]. It offers a fluent way to configure the [ApolloRequest].
 *
 * Contrary to an [ApolloRequest], an [ApolloCall] doesn't have a request id and a new request id is allocated every time [execute] or [toFlow] is called.
 *
 * - call [execute] for simple cases that expect a single response:
 *
 * ```
 * val response = apolloClient.query(myQuery).fetchPolicy(CacheOnly).execute()
 * ```
 *
 *
 * - call [toFlow] for other cases that expect multiple [ApolloResponse] like subscriptions and `@defer`:
 *
 * ```
 * apolloClient.subscription(mySubscription).toFlow().collect { response ->
 *   println(response.data)
 * }
 * ```
 *
 * @see execute
 * @see toFlow
 */
class ApolloCall internal constructor(
    internal val apolloClient: ApolloClient,
    private val requestBuilder: ApolloRequest.Builder,
) : MutableExecutionOptions> {

  internal constructor(apolloClient: ApolloClient, operation: Operation) : this(apolloClient, ApolloRequest.Builder(operation))

  val operation: Operation get() = requestBuilder.operation
  override val executionContext: ExecutionContext get() = requestBuilder.executionContext
  override val httpMethod: HttpMethod? get() = requestBuilder.httpMethod
  override val sendApqExtensions: Boolean? get() = requestBuilder.sendApqExtensions
  override val sendDocument: Boolean? get() = requestBuilder.sendDocument
  override val enableAutoPersistedQueries: Boolean? get() = requestBuilder.enableAutoPersistedQueries
  override val canBeBatched: Boolean? get() = requestBuilder.canBeBatched
  override val httpHeaders: List? get() = requestBuilder.httpHeaders
  val ignoreApolloClientHttpHeaders: Boolean? get() = requestBuilder.ignoreApolloClientHttpHeaders
  @ApolloExperimental
  val retryOnError: Boolean? get() = requestBuilder.retryOnError
  @ApolloExperimental
  val failFastIfOffline: Boolean? get() = requestBuilder.failFastIfOffline

  fun failFastIfOffline(failFastIfOffline: Boolean?) = apply {
    requestBuilder.failFastIfOffline(failFastIfOffline)
  }

  override fun addExecutionContext(executionContext: ExecutionContext) = apply {
    requestBuilder.addExecutionContext(executionContext)
  }

  override fun httpMethod(httpMethod: HttpMethod?) = apply {
    requestBuilder.httpMethod(httpMethod)
  }

  override fun httpHeaders(httpHeaders: List?) = apply {
    requestBuilder.httpHeaders(httpHeaders)
  }

  override fun addHttpHeader(name: String, value: String) = apply {
    requestBuilder.addHttpHeader(name, value)
  }

  override fun sendApqExtensions(sendApqExtensions: Boolean?) = apply {
    requestBuilder.sendApqExtensions(sendApqExtensions)
  }

  override fun sendDocument(sendDocument: Boolean?) = apply {
    requestBuilder.sendDocument(sendDocument)
  }

  override fun enableAutoPersistedQueries(enableAutoPersistedQueries: Boolean?) = apply {
    requestBuilder.enableAutoPersistedQueries(enableAutoPersistedQueries)
  }

  override fun canBeBatched(canBeBatched: Boolean?) = apply {
    requestBuilder.canBeBatched(canBeBatched)
  }

  @ApolloExperimental
  fun retryOnError(retryOnError: Boolean?): ApolloCall = apply {
    requestBuilder.retryOnError(retryOnError)
  }

  fun ignoreApolloClientHttpHeaders(ignoreApolloClientHttpHeaders: Boolean?) = apply {
    requestBuilder.ignoreApolloClientHttpHeaders(ignoreApolloClientHttpHeaders)
  }

  fun copy(): ApolloCall {
    return ApolloCall(apolloClient, requestBuilder.build().newBuilder())
  }

  /**
   * Returns a cold Flow that produces [ApolloResponse]s from this [ApolloCall].
   *
   * The returned [Flow] does not throw on I/O errors or cache misses. Errors are not always terminal and some can be recovered. Check [ApolloResponse.exception] to handle errors:
   *
   * ```
   * apolloClient.subscription(NewOrders())
   *                  .toFlow()
   *                  .collect {
   *                    if (it.data != null) {
   *                      // Handle (potentially partial) data
   *                    } else {
   *                      // Something wrong happened
   *                      if (it.exception != null) {
   *                        // Handle fetch errors
   *                      } else {
   *                        // Handle GraphQL errors in response.errors
   *                      }
   *                    }
   *                  }
   * ```
   *
   * The returned [Flow] flows on the dispatcher configured in [ApolloClient.Builder.dispatcher] or a default dispatcher else. There is no need to change the coroutine context before calling [toFlow]. See [ApolloClient.Builder.dispatcher] for more details.
   *
   * The returned [Flow] has [kotlinx.coroutines.channels.Channel.UNLIMITED] buffering so that no response is missed in the case of a slow consumer. Use [kotlinx.coroutines.flow.buffer] to change that behaviour.
   *
   * @see toFlowV3
   * @see ApolloClient.Builder.dispatcher
   */
  fun toFlow(): Flow> {
    return apolloClient.executeAsFlowInternal(requestBuilder.build(), false)
  }

  /**
   * A version of [execute] that restores 3.x behaviour:
   * - throw on fetch errors.
   * - make `CacheFirst`, `NetworkFirst` and `CacheAndNetwork` policies ignore fetch errors.
   * - throw ApolloComposite exception if needed.
   */
  @Deprecated("Use toFlow() and handle ApolloResponse.exception instead")
  @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_0)
  fun toFlowV3(): Flow> {
    @Suppress("DEPRECATION")
    return conflateFetchPolicyInterceptorResponses(true)
        .apolloClient
        .executeAsFlowInternal(requestBuilder.build(), true)
  }

  /**
   * A version of [execute] that restores 3.x behaviour:
   * - throw on fetch errors.
   * - make `CacheFirst`, `NetworkFirst` and `CacheAndNetwork` policies ignore fetch errors.
   * - throw ApolloComposite exception if needed.
   */
  @Deprecated("Use execute() and handle ApolloResponse.exception instead")
  @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_0)
  suspend fun executeV3(): ApolloResponse {
    @Suppress("DEPRECATION")
    return singleSuccessOrException(toFlowV3())
  }

  /**
   * Retrieves a single [ApolloResponse] from this [ApolloCall].
   *
   * [execute] calls [toFlow] and filters out cache or network errors to return a single success [ApolloResponse].
   *
   * [execute] throws if more than one success [ApolloResponse] is returned, for an example, if [operation] is a subscription or a `@defer` query.
   * In those cases use [toFlow] instead.
   *
   * [execute] may fail due to an I/O error, a cache miss or other reasons. In that case, check [ApolloResponse.exception]:
   * ```
   * val response = apolloClient.execute(ProductQuery())
   * if (response.data != null) {
   *   // Handle (potentially partial) data
   * } else {
   *   // Something wrong happened
   *   if (it.exception != null) {
   *     // Handle fetch errors
   *   } else {
   *     // Handle GraphQL errors in response.errors
   *   }
   * }
   * ```
   *
   * The work is executed on the dispatcher configured in [ApolloClient.Builder.dispatcher] or a default dispatcher else. There is no need to change the coroutine context before calling [execute]. See [ApolloClient.Builder.dispatcher] for more details.
   *
   * @throws ApolloException if the call returns zero or multiple valid GraphQL responses.
   *
   * @see executeV3
   * @see ApolloClient.Builder.dispatcher
   */
  suspend fun execute(): ApolloResponse {
    return singleSuccessOrException(toFlow())
  }

  private suspend fun singleSuccessOrException(flow: Flow>): ApolloResponse {
    val responses = flow.toList()
    val (exceptionResponses, successResponses) = responses.partition { it.exception != null }
    return when (successResponses.size) {
      0 -> {
        when (exceptionResponses.size) {
          0 -> throw DefaultApolloException("The operation did not emit any item, check your interceptor chain")
          1 -> exceptionResponses.first()
          else -> {
            val first = exceptionResponses.first()
            first.newBuilder()
                .exception(
                    exceptionResponses.drop(1).fold(first.exception!!) { acc, response ->
                      acc.also {
                        it.addSuppressed(response.exception!!)
                      }
                    }
                )
                .build()
          }
        }
      }

      1 -> successResponses.first()
      else -> throw DefaultApolloException("The operation returned multiple items, use .toFlow() instead of .execute()")
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy