Skip to content

Add ApolloClient.Builder.cache() extension generation #181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ package com.apollographql.cache.apollocompilerplugin.internal

import com.apollographql.apollo.annotations.ApolloExperimental
import com.apollographql.apollo.ast.GQLDocument
import com.apollographql.apollo.ast.GQLInterfaceTypeDefinition
import com.apollographql.apollo.ast.GQLObjectTypeDefinition
import com.apollographql.apollo.ast.GQLStringValue
import com.apollographql.apollo.ast.GQLUnionTypeDefinition
import com.apollographql.apollo.ast.Schema
import com.apollographql.apollo.ast.Schema.Companion.TYPE_POLICY
import com.apollographql.apollo.ast.toSchema
import com.apollographql.apollo.compiler.ApolloCompilerPluginEnvironment
import com.apollographql.apollo.compiler.SchemaCodeGenerator
import com.apollographql.cache.apollocompilerplugin.VERSION
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.MAP
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.SET
import com.squareup.kotlinpoet.STRING
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
Expand All @@ -29,6 +38,11 @@ private object Symbols {
val MaxAgeDuration = MaxAge.nestedClass("Duration")
val Seconds = MemberName(Duration.Companion::class.asTypeName(), "seconds", isExtension = true)
val TypePolicy = ClassName("com.apollographql.cache.normalized.api", "TypePolicy")
val ApolloClientBuilder = ClassName("com.apollographql.apollo", "ApolloClient", "Builder")
val NormalizedCacheFactory = ClassName("com.apollographql.cache.normalized.api", "NormalizedCacheFactory")
val CacheKeyScope = ClassName("com.apollographql.cache.normalized.api", "CacheKey", "Scope")
val KotlinDuration = Duration::class.asTypeName()
val NormalisedCacheExtension = MemberName("com.apollographql.cache.normalized", "normalizedCache", isExtension = true)
}

internal class CacheSchemaCodeGenerator(
Expand All @@ -43,6 +57,8 @@ internal class CacheSchemaCodeGenerator(
TypeSpec.objectBuilder("Cache")
.addProperty(maxAgeProperty(validSchema))
.addProperty(typePoliciesProperty(validSchema))
.addProperty(connectionTypesProperty(validSchema, packageName))
.addFunction(cacheFunction(validSchema))
.build()
)
.addFileComment(
Expand Down Expand Up @@ -111,4 +127,69 @@ internal class CacheSchemaCodeGenerator(
.initializer(initializer)
.build()
}

private fun connectionTypesProperty(schema: Schema, packageName: String): PropertySpec {
// TODO: connectionTypes is generated by the Apollo compiler for now, and we just reference it. Instead we should generate it here.
val hasPagination = schema.hasConnectionFields()
val initializer = if (hasPagination) {
val paginationPackageName = packageName.substringBeforeLast(".") + ".pagination"
CodeBlock.of("%T.connectionTypes", ClassName(paginationPackageName, "Pagination"))
} else {
CodeBlock.of("emptySet()")
}
return PropertySpec.builder(
name = "connectionTypes",
type = SET.parameterizedBy(STRING)
)
.initializer(initializer)
.build()
}

private fun cacheFunction(validSchema: Schema): FunSpec {
validSchema.hasConnectionFields()
return FunSpec.builder("cache")
.receiver(Symbols.ApolloClientBuilder)
.addParameter("normalizedCacheFactory", Symbols.NormalizedCacheFactory)
.addParameter(ParameterSpec.builder("keyScope", Symbols.CacheKeyScope)
.defaultValue("CacheKey.Scope.TYPE").build()
)
.addParameter(ParameterSpec.builder("defaultMaxAge", Symbols.KotlinDuration)
.defaultValue("%T.INFINITE", Symbols.KotlinDuration)
.build()
)
.addParameter(ParameterSpec.builder("writeToCacheAsynchronously", BOOLEAN)
.defaultValue("false")
.build()
)
.returns(Symbols.ApolloClientBuilder)
.addCode(
CodeBlock.builder()
.addStatement(
"return %M(\n⇥" +
"normalizedCacheFactory = normalizedCacheFactory,\n" +
"typePolicies = typePolicies,\n" +
"connectionTypes = connectionTypes, \n" +
"maxAges = maxAges,\n" +
"defaultMaxAge = defaultMaxAge,\n" +
"keyScope = keyScope,\n" +
"writeToCacheAsynchronously = writeToCacheAsynchronously,\n⇤" +
")",
Symbols.NormalisedCacheExtension,
)
.build()
)
.build()
}

private fun Schema.hasConnectionFields(): Boolean {
val directives = typeDefinitions.values.filterIsInstance<GQLObjectTypeDefinition>().flatMap { it.directives } +
typeDefinitions.values.filterIsInstance<GQLInterfaceTypeDefinition>().flatMap { it.directives } +
typeDefinitions.values.filterIsInstance<GQLUnionTypeDefinition>().flatMap { it.directives }
return directives.any {
originalDirectiveName(it.name) == TYPE_POLICY &&
it.arguments.any { arg ->
arg.name == "connectionFields" && !(arg.value as? GQLStringValue)?.value.isNullOrBlank()
}
}
}
}
1 change: 1 addition & 0 deletions normalized-cache/api/normalized-cache.api
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ public final class com/apollographql/cache/normalized/NormalizedCache {
public static final fun isFromCache (Lcom/apollographql/apollo/api/ApolloResponse;)Z
public static final fun maxStale-HG0u8IE (Lcom/apollographql/apollo/api/MutableExecutionOptions;J)Ljava/lang/Object;
public static final fun memoryCacheOnly (Lcom/apollographql/apollo/api/MutableExecutionOptions;Z)Ljava/lang/Object;
public static final fun normalizedCache-HOFDmpg (Lcom/apollographql/apollo/ApolloClient$Builder;Lcom/apollographql/cache/normalized/api/NormalizedCacheFactory;Ljava/util/Map;Ljava/util/Set;Ljava/util/Map;JLcom/apollographql/cache/normalized/api/CacheKey$Scope;Z)Lcom/apollographql/apollo/ApolloClient$Builder;
public static final fun optimisticUpdates (Lcom/apollographql/apollo/ApolloCall;Lcom/apollographql/apollo/api/Mutation$Data;)Lcom/apollographql/apollo/ApolloCall;
public static final fun optimisticUpdates (Lcom/apollographql/apollo/api/ApolloRequest$Builder;Lcom/apollographql/apollo/api/Mutation$Data;)Lcom/apollographql/apollo/api/ApolloRequest$Builder;
public static final fun refetchPolicy (Lcom/apollographql/apollo/api/MutableExecutionOptions;Lcom/apollographql/cache/normalized/FetchPolicy;)Ljava/lang/Object;
Expand Down
1 change: 1 addition & 0 deletions normalized-cache/api/normalized-cache.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ final val com.apollographql.cache.normalized/isFromCache // com.apollographql.ca
final fun (com.apollographql.apollo/ApolloClient.Builder).com.apollographql.cache.normalized/cacheManager(com.apollographql.cache.normalized/CacheManager, kotlin/Boolean = ...): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.cache.normalized/cacheManager|cacheManager@com.apollographql.apollo.ApolloClient.Builder(com.apollographql.cache.normalized.CacheManager;kotlin.Boolean){}[0]
final fun (com.apollographql.apollo/ApolloClient.Builder).com.apollographql.cache.normalized/logCacheMisses(kotlin/Function1<kotlin/String, kotlin/Unit> = ...): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.cache.normalized/logCacheMisses|logCacheMisses@com.apollographql.apollo.ApolloClient.Builder(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
final fun (com.apollographql.apollo/ApolloClient.Builder).com.apollographql.cache.normalized/normalizedCache(com.apollographql.cache.normalized.api/NormalizedCacheFactory, com.apollographql.cache.normalized.api/CacheKeyGenerator = ..., com.apollographql.cache.normalized.api/MetadataGenerator = ..., com.apollographql.cache.normalized.api/CacheResolver = ..., com.apollographql.cache.normalized.api/RecordMerger = ..., com.apollographql.cache.normalized.api/FieldKeyGenerator = ..., com.apollographql.cache.normalized.api/EmbeddedFieldsProvider = ..., com.apollographql.cache.normalized.api/MaxAgeProvider = ..., kotlin/Boolean = ...): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.cache.normalized/normalizedCache|normalizedCache@com.apollographql.apollo.ApolloClient.Builder(com.apollographql.cache.normalized.api.NormalizedCacheFactory;com.apollographql.cache.normalized.api.CacheKeyGenerator;com.apollographql.cache.normalized.api.MetadataGenerator;com.apollographql.cache.normalized.api.CacheResolver;com.apollographql.cache.normalized.api.RecordMerger;com.apollographql.cache.normalized.api.FieldKeyGenerator;com.apollographql.cache.normalized.api.EmbeddedFieldsProvider;com.apollographql.cache.normalized.api.MaxAgeProvider;kotlin.Boolean){}[0]
final fun (com.apollographql.apollo/ApolloClient.Builder).com.apollographql.cache.normalized/normalizedCache(com.apollographql.cache.normalized.api/NormalizedCacheFactory, kotlin.collections/Map<kotlin/String, com.apollographql.cache.normalized.api/TypePolicy>, kotlin.collections/Set<kotlin/String>, kotlin.collections/Map<kotlin/String, com.apollographql.cache.normalized.api/MaxAge>, kotlin.time/Duration, com.apollographql.cache.normalized.api/CacheKey.Scope, kotlin/Boolean): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.cache.normalized/normalizedCache|normalizedCache@com.apollographql.apollo.ApolloClient.Builder(com.apollographql.cache.normalized.api.NormalizedCacheFactory;kotlin.collections.Map<kotlin.String,com.apollographql.cache.normalized.api.TypePolicy>;kotlin.collections.Set<kotlin.String>;kotlin.collections.Map<kotlin.String,com.apollographql.cache.normalized.api.MaxAge>;kotlin.time.Duration;com.apollographql.cache.normalized.api.CacheKey.Scope;kotlin.Boolean){}[0]
final fun (com.apollographql.cache.normalized.api/Record).com.apollographql.cache.normalized.api/expirationDate(kotlin/String): kotlin/Long? // com.apollographql.cache.normalized.api/expirationDate|expirationDate@com.apollographql.cache.normalized.api.Record(kotlin.String){}[0]
final fun (com.apollographql.cache.normalized.api/Record).com.apollographql.cache.normalized.api/receivedDate(kotlin/String): kotlin/Long? // com.apollographql.cache.normalized.api/receivedDate|receivedDate@com.apollographql.cache.normalized.api.Record(kotlin.String){}[0]
final fun (com.apollographql.cache.normalized.api/Record).com.apollographql.cache.normalized.api/withDates(kotlin/String?, kotlin/String?): com.apollographql.cache.normalized.api/Record // com.apollographql.cache.normalized.api/withDates|withDates@com.apollographql.cache.normalized.api.Record(kotlin.String?;kotlin.String?){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,30 @@ import com.apollographql.apollo.interceptor.ApolloInterceptorChain
import com.apollographql.apollo.mpp.currentTimeMillis
import com.apollographql.apollo.network.http.HttpInfo
import com.apollographql.cache.normalized.api.ApolloCacheHeaders
import com.apollographql.cache.normalized.api.CacheControlCacheResolver
import com.apollographql.cache.normalized.api.CacheHeaders
import com.apollographql.cache.normalized.api.CacheKey
import com.apollographql.cache.normalized.api.CacheKeyGenerator
import com.apollographql.cache.normalized.api.CacheKeyGeneratorContext
import com.apollographql.cache.normalized.api.CacheResolver
import com.apollographql.cache.normalized.api.ConnectionMetadataGenerator
import com.apollographql.cache.normalized.api.ConnectionRecordMerger
import com.apollographql.cache.normalized.api.DefaultEmbeddedFieldsProvider
import com.apollographql.cache.normalized.api.DefaultFieldKeyGenerator
import com.apollographql.cache.normalized.api.DefaultMaxAgeProvider
import com.apollographql.cache.normalized.api.DefaultRecordMerger
import com.apollographql.cache.normalized.api.EmbeddedFieldsProvider
import com.apollographql.cache.normalized.api.EmptyMetadataGenerator
import com.apollographql.cache.normalized.api.FieldKeyGenerator
import com.apollographql.cache.normalized.api.FieldPolicyCacheResolver
import com.apollographql.cache.normalized.api.GlobalMaxAgeProvider
import com.apollographql.cache.normalized.api.MaxAge
import com.apollographql.cache.normalized.api.MaxAgeProvider
import com.apollographql.cache.normalized.api.MetadataGenerator
import com.apollographql.cache.normalized.api.NormalizedCacheFactory
import com.apollographql.cache.normalized.api.RecordMerger
import com.apollographql.cache.normalized.api.SchemaCoordinatesMaxAgeProvider
import com.apollographql.cache.normalized.api.TypePolicy
import com.apollographql.cache.normalized.api.TypePolicyCacheKeyGenerator
import com.apollographql.cache.normalized.internal.ApolloCacheInterceptor
import com.apollographql.cache.normalized.internal.WatcherInterceptor
Expand Down Expand Up @@ -67,7 +76,7 @@ fun ApolloClient.Builder.normalizedCache(
normalizedCacheFactory: NormalizedCacheFactory,
cacheKeyGenerator: CacheKeyGenerator = @Suppress("DEPRECATION") TypePolicyCacheKeyGenerator,
metadataGenerator: MetadataGenerator = EmptyMetadataGenerator,
cacheResolver: CacheResolver = com.apollographql.cache.normalized.api.FieldPolicyCacheResolver(keyScope = CacheKey.Scope.TYPE),
cacheResolver: CacheResolver = FieldPolicyCacheResolver(keyScope = CacheKey.Scope.TYPE),
recordMerger: RecordMerger = DefaultRecordMerger,
fieldKeyGenerator: FieldKeyGenerator = DefaultFieldKeyGenerator,
embeddedFieldsProvider: EmbeddedFieldsProvider = DefaultEmbeddedFieldsProvider,
Expand All @@ -88,6 +97,58 @@ fun ApolloClient.Builder.normalizedCache(
)
}

fun ApolloClient.Builder.normalizedCache(
normalizedCacheFactory: NormalizedCacheFactory,
typePolicies: Map<String, TypePolicy>,
connectionTypes: Set<String>,
maxAges: Map<String, MaxAge>,
defaultMaxAge: Duration,
keyScope: CacheKey.Scope,
writeToCacheAsynchronously: Boolean,
): ApolloClient.Builder {
val cacheKeyGenerator = if (typePolicies.isEmpty()) {
object : CacheKeyGenerator {
override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? {
return null
}
}
} else {
TypePolicyCacheKeyGenerator(typePolicies, keyScope)
}
val metadataGenerator = if (connectionTypes.isEmpty()) {
EmptyMetadataGenerator
} else {
ConnectionMetadataGenerator(connectionTypes)
}
val maxAgeProvider = if (maxAges.isEmpty()) {
GlobalMaxAgeProvider(defaultMaxAge)
} else {
SchemaCoordinatesMaxAgeProvider(maxAges, defaultMaxAge)
}
val cacheResolver = if (maxAges.isEmpty()) {
FieldPolicyCacheResolver(keyScope)
} else {
CacheControlCacheResolver(
maxAgeProvider = maxAgeProvider,
delegateResolver = FieldPolicyCacheResolver(keyScope),
)
}
val recordMerger = if (connectionTypes.isEmpty()) {
DefaultRecordMerger
} else {
ConnectionRecordMerger
}
return normalizedCache(
normalizedCacheFactory = normalizedCacheFactory,
cacheKeyGenerator = cacheKeyGenerator,
metadataGenerator = metadataGenerator,
cacheResolver = cacheResolver,
recordMerger = recordMerger,
maxAgeProvider = maxAgeProvider,
writeToCacheAsynchronously = writeToCacheAsynchronously,
)
}

@JvmName("-logCacheMisses")
fun ApolloClient.Builder.logCacheMisses(
log: (String) -> Unit = { println(it) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@ import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.Optional
import com.apollographql.cache.normalized.FetchPolicy
import com.apollographql.cache.normalized.api.CacheKey
import com.apollographql.cache.normalized.api.ConnectionMetadataGenerator
import com.apollographql.cache.normalized.api.ConnectionRecordMerger
import com.apollographql.cache.normalized.api.TypePolicyCacheKeyGenerator
import com.apollographql.cache.normalized.fetchPolicy
import com.apollographql.cache.normalized.memory.MemoryCacheFactory
import com.apollographql.cache.normalized.normalizedCache
import com.apollographql.cache.normalized.sql.SqlNormalizedCacheFactory
import com.example.browsersample.BuildConfig
import com.example.browsersample.graphql.RepositoryListQuery
import com.example.browsersample.graphql.cache.Cache
import com.example.browsersample.graphql.pagination.Pagination
import com.example.browsersample.graphql.cache.Cache.cache
import org.w3c.dom.Worker

private const val SERVER_URL = "https://api.github.com/graphql"
Expand All @@ -35,26 +30,21 @@ val apolloClient: ApolloClient by lazy {
val memoryThenSqlCache = memoryCache.chain(sqlCache)

ApolloClient.Builder()
.serverUrl(SERVER_URL)
.serverUrl(SERVER_URL)

// Add headers for authentication
.addHttpHeader(
HEADER_AUTHORIZATION,
"$HEADER_AUTHORIZATION_BEARER ${BuildConfig.GITHUB_OAUTH_KEY}"
)
// Add headers for authentication
.addHttpHeader(
HEADER_AUTHORIZATION,
"$HEADER_AUTHORIZATION_BEARER ${BuildConfig.GITHUB_OAUTH_KEY}"
)

// Normalized cache
.normalizedCache(
normalizedCacheFactory = memoryThenSqlCache,
cacheKeyGenerator = TypePolicyCacheKeyGenerator(
typePolicies = Cache.typePolicies,
keyScope = CacheKey.Scope.SERVICE,
),
metadataGenerator = ConnectionMetadataGenerator(Pagination.connectionTypes),
recordMerger = ConnectionRecordMerger
)
// Normalized cache
.cache(
normalizedCacheFactory = memoryThenSqlCache,
keyScope = CacheKey.Scope.SERVICE,
)

.build()
.build()
}

suspend fun fetchAndMergeNextPage() {
Expand All @@ -65,5 +55,5 @@ suspend fun fetchAndMergeNextPage() {
// 2. Fetch the next page from the network and store it in the cache
val after = cacheResponse.data!!.organization!!.repositories.pageInfo.endCursor
apolloClient.query(RepositoryListQuery(after = Optional.presentIfNotNull(after)))
.fetchPolicy(FetchPolicy.NetworkOnly).execute()
.fetchPolicy(FetchPolicy.NetworkOnly).execute()
}
4 changes: 2 additions & 2 deletions samples/pagination/manual/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ apollo {
service("main") {
packageName.set("com.example.apollokotlinpaginationsample.graphql")

plugin("com.apollographql.cache:normalized-cache-apollo-compiler-plugin:1.0.0-alpha.3") {
plugin("com.apollographql.cache:normalized-cache-apollo-compiler-plugin:1.0.0-alpha.4-SNAPSHOT") {
argument("packageName", packageName.get())
}

Expand All @@ -88,7 +88,7 @@ dependencies {
implementation("androidx.compose.material3:material3")

implementation("com.apollographql.apollo:apollo-runtime")
implementation("com.apollographql.cache:normalized-cache-sqlite:1.0.0-alpha.3")
implementation("com.apollographql.cache:normalized-cache-sqlite:1.0.0-alpha.4-SNAPSHOT")

debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.example.apollokotlinpaginationsample.Application
import com.example.apollokotlinpaginationsample.BuildConfig
import com.example.apollokotlinpaginationsample.graphql.RepositoryListQuery
import com.example.apollokotlinpaginationsample.graphql.cache.Cache
import com.example.apollokotlinpaginationsample.graphql.cache.Cache.cache

private const val SERVER_URL = "https://api.github.com/graphql"

Expand All @@ -35,12 +36,9 @@ val apolloClient: ApolloClient by lazy {
)

// Normalized cache
.normalizedCache(
.cache(
memoryThenSqlCache,
cacheKeyGenerator = TypePolicyCacheKeyGenerator(
typePolicies = Cache.typePolicies,
keyScope = CacheKey.Scope.SERVICE,
),
keyScope = CacheKey.Scope.SERVICE,
)

.build()
Expand Down
Loading