Skip to content

Apollo Kotlin normalized cache does not trigger watch() updates for nested objects after mutation #337

@premjit-develops

Description

@premjit-develops

I’m using Apollo Kotlin with a normalized cache (Memory + SQL cache chaining) in a Supabase GraphQL setup. For simple (flat) queries and mutations, the cache updates correctly and watch() emits updates to the UI as expected.

However, when dealing with nested objects, specifically a favorites → quotes → nested collections structure, mutations (insert/delete) do not trigger UI updates via watch() even though the mutation returns the same nested structure.

I'm trying to understand whether this is:

  • a limitation/bug in Apollo normalized cache
  • or an issue with my cache configuration / schema policies

Environment

  • Apollo Kotlin: 4.4.2
  • Normalized cache: 1.0.1
  • Backend: Supabase

Cache Configuration

val chainedCacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024)
    .chain(sqlCacheFactory)

install(GraphQL) {
    apolloConfiguration {
        cache(
            normalizedCacheFactory = chainedCacheFactory,
            keyScope = CacheKey.Scope.SERVICE
        )
    }
}

Type Policies

extend type Query @fieldPolicy(forField: "favoritesCollection", keyArgs: "filter")
extend type favorites @typePolicy(keyFields: "id quote_id") @cacheControl(maxAge: 7776000)

extend type Query @fieldPolicy(forField: "quotesCollection", keyArgs: "filter")
extend type quotes @typePolicy(keyFields: "id") @cacheControl(maxAge: 7776000)

extend type Query @fieldPolicy(forField: "book_sectionsCollection", keyArgs: "filter")
extend type book_sections @typePolicy(keyFields: "id") @cacheControl(maxAge: 7776000)

extend type Query @fieldPolicy(forField: "booksCollection", keyArgs: "filter")
extend type books @typePolicy(keyFields: "id") @cacheControl(maxAge: 7776000)

extend type Query @fieldPolicy(forField: "quote_questionsCollection", keyArgs: "filter")
extend type quote_questions @typePolicy(keyFields: "id") @cacheControl(maxAge: 7776000)

Query

query GetFavoriteQuotes($userId: UUID!, $limit: Int, $offset: Int) {
  favoritesCollection(
    filter: { id: { eq: $userId } }
    first: $limit
    offset: $offset
  ) {
    edges {
      node {
        id
        quote_id
        __typename
        quotes {
          id
          __typename
          book_sectionsCollection {
            edges {
              node {
                books {
                  id
                  name
                  __typename
                }
              }
            }
          }
          quote_questionsCollection {
            edges {
              node {
                id
                question_text
                __typename
              }
            }
          }
        }
      }
    }
  }
}

Mutations

Insert

mutation InsertFavoriteQuote($userId: UUID!, $quoteId: Int!) {
  insertIntofavoritesCollection(objects: { id: $userId, quote_id: $quoteId }) {
    records {
      id
      quote_id
      __typename
      quotes {
        id
        __typename
        ...
      }
    }
  }
}

Delete

mutation DeleteFavoriteQuote($userId: UUID!, $quoteId: Int!) {
  deleteFromfavoritesCollection(
    filter: { id: { eq: $userId }, quote_id: { eq: $quoteId } }
    atMost: 1
  ) {
    records {
      id
      quote_id
      __typename
      quotes {
        id
        __typename
        ...
      }
    }
  }
}

Repository Code

override suspend fun toggleFavorite(
    userId: String,
    quoteId: Int,
    isFavorite: Boolean
): AppResult<Unit> = safeCall(graphqlResolver) {

    if (isFavorite) {
        apolloClient.mutation(DeleteFavoriteQuoteMutation(userId, quoteId)).execute()
    } else {
        apolloClient.mutation(InsertFavoriteQuoteMutation(userId, quoteId)).execute()
    }
}
override fun getFavoriteQuotes(
    userId: String
): Flow<AppResult<List<FavoriteQuote>?>> =
    apolloClient
        .query(GetFavoriteQuotesQuery(userId, limit = Optional.present(100), offset = Optional.present(0)))
        .fetchPolicy(FetchPolicy.CacheAndNetwork)
        .watch()
        .mapNotNull { response ->
            val data = response.requireDataOrThrow()
            data.favoritesCollection?.edges?.map { it.node.toFavoriteQuote() }
        }
        .asResultFlow(graphqlResolver)

Expected Behavior

After inserting or deleting a favorite:

  • The normalized cache should update
  • watch() on GetFavoriteQuotesQuery should emit updated data
  • UI should reflect changes immediately

Actual Behavior

  • Cache updates correctly for flat queries

  • For nested favorites query:

    • No emission from watch()
    • UI does not update unless query is manually refetched

Metadata

Metadata

Assignees

No one assigned

    Labels

    ⌛ Waiting for infoMore information is requiredquestionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions