Skip to content

Introduce CacheKey.Scope. #157

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 3 commits into from
May 19, 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 @@ -13,7 +13,7 @@ internal class ApolloCacheCompilerPlugin(
private val environment: ApolloCompilerPluginEnvironment,
) : ApolloCompilerPlugin {
override fun foreignSchemas(): List<ForeignSchema> {
return listOf(ForeignSchema("cache", "v0.1", cacheControlGQLDefinitions))
return listOf(ForeignSchema("cache", "v0.1", cacheGQLDefinitions))
}

override fun schemaListener(): SchemaListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import com.squareup.kotlinpoet.MAP
import com.squareup.kotlinpoet.MemberName
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
import com.squareup.kotlinpoet.withIndent
import java.io.File
import kotlin.time.Duration

Expand All @@ -26,6 +26,7 @@ private object Symbols {
val MaxAgeInherit = MaxAge.nestedClass("Inherit")
val MaxAgeDuration = MaxAge.nestedClass("Duration")
val Seconds = MemberName(Duration.Companion::class.asTypeName(), "seconds", isExtension = true)
val TypePolicy = ClassName("com.apollographql.cache.normalized.api", "TypePolicy")
}

internal class CacheSchemaListener(
Expand All @@ -34,13 +35,11 @@ internal class CacheSchemaListener(
override fun onSchema(schema: Schema, outputDirectory: File) {
val packageName = (environment.arguments["packageName"] as? String
?: throw IllegalArgumentException("packageName argument is required and must be a String")) + ".cache"
val maxAgeProperty = maxAgeProperty(schema)
val keyFieldsProperty = keyFieldsProperty(schema)
val file = FileSpec.builder(packageName, "Cache")
.addType(
TypeSpec.objectBuilder("Cache")
.addProperty(maxAgeProperty)
.addProperty(keyFieldsProperty)
.addProperty(maxAgeProperty(schema))
.addProperty(typePoliciesProperty(schema))
.build()
)
.addFileComment(
Expand All @@ -53,56 +52,60 @@ internal class CacheSchemaListener(
""".trimIndent()
)
.build()

file.writeTo(outputDirectory)
}

private fun maxAgeProperty(schema: Schema): PropertySpec {
val maxAges = schema.getMaxAges(environment.logger)
val initializer = CodeBlock.builder().apply {
add("mapOf(\n")
indent()
maxAges.forEach { (field, duration) ->
if (duration == -1) {
addStatement("%S to %T,", field, Symbols.MaxAgeInherit)
} else {
addStatement("%S to %T(%L.%M),", field, Symbols.MaxAgeDuration, duration, Symbols.Seconds)
withIndent {
maxAges.forEach { (field, duration) ->
if (duration == -1) {
addStatement("%S to %T,", field, Symbols.MaxAgeInherit)
} else {
addStatement("%S to %T(%L.%M),", field, Symbols.MaxAgeDuration, duration, Symbols.Seconds)
}
}
}
unindent()
add(")")
}
.build()
return PropertySpec.Companion.builder("maxAges", MAP
.parameterizedBy(STRING, Symbols.MaxAge)
return PropertySpec.Companion.builder(
name = "maxAges",
type = MAP.parameterizedBy(STRING, Symbols.MaxAge)
)
.initializer(initializer)
.build()
}

private fun keyFieldsProperty(schema: Schema): PropertySpec {
val keyFields = schema.getObjectKeyFields()
private fun typePoliciesProperty(schema: Schema): PropertySpec {
val typePolicies = schema.getTypePolicies()
val initializer = CodeBlock.builder().apply {
add("mapOf(\n")
indent()
keyFields.forEach { (type, fields) ->
addStatement("%S to setOf(", type)
indent()
fields.forEach { field ->
addStatement("%S,", field)
withIndent {
typePolicies.forEach { (type, typePolicy) ->
addStatement("%S to %T(", type, Symbols.TypePolicy)
withIndent {
addStatement("keyFields = setOf(")
withIndent {
typePolicy.keyFields.forEach { keyField ->
addStatement("%S, ", keyField)
}
}
add("),\n")
}
addStatement("),")
}
unindent()
addStatement("),")
}
unindent()
add(")")
}
.build()
return PropertySpec.builder("keyFields", MAP
.parameterizedBy(STRING, SET.parameterizedBy(STRING))
return PropertySpec.builder(
name = "typePolicies",
type = MAP.parameterizedBy(STRING, Symbols.TypePolicy)
)
.initializer(initializer)
.build()
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.apollographql.cache.apollocompilerplugin.internal

import com.apollographql.apollo.ast.parseAsGQLDocument

private val cacheControlDefinitions_0_1 = """
private val cacheDefinitions_0_1 = """
""${'"'}
Possible values for the `@cacheControl` `scope` argument (unused on the client).
""${'"'}
Expand Down Expand Up @@ -64,4 +64,4 @@ private val cacheControlDefinitions_0_1 = """
) repeatable on OBJECT | INTERFACE
""".trimIndent()

internal val cacheControlGQLDefinitions = cacheControlDefinitions_0_1.parseAsGQLDocument().getOrThrow().definitions
internal val cacheGQLDefinitions = cacheDefinitions_0_1.parseAsGQLDocument().getOrThrow().definitions

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.apollographql.cache.apollocompilerplugin.internal

import com.apollographql.apollo.ast.GQLDirective
import com.apollographql.apollo.ast.GQLField
import com.apollographql.apollo.ast.GQLInterfaceTypeDefinition
import com.apollographql.apollo.ast.GQLObjectTypeDefinition
import com.apollographql.apollo.ast.GQLStringValue
import com.apollographql.apollo.ast.GQLTypeDefinition
import com.apollographql.apollo.ast.Schema
import com.apollographql.apollo.ast.Schema.Companion.TYPE_POLICY
import com.apollographql.apollo.ast.SourceAwareException
import com.apollographql.apollo.ast.parseAsGQLSelections

internal data class TypePolicy(
val keyFields: Set<String>,
)

/**
* Returns the type policies for object types and interfaces in the schema.
*/
internal fun Schema.getTypePolicies(): Map<String, TypePolicy> {
val typePolicyCache = mutableMapOf<String, TypePolicy?>()
@Suppress("UNCHECKED_CAST")
return typeDefinitions.values
.filter { it is GQLObjectTypeDefinition || it is GQLInterfaceTypeDefinition }
.associate {
it.name to validateAndComputeTypePolicy(it, typePolicyCache)
}
.filterValues { it != null } as Map<String, TypePolicy>
}

/**
* Returns the type policy for this type definition.
*
* If an interface defines a type policy, its subtypes inherit that type policy. It is an error trying to redefine the type policy in a subtype.
*/
private fun Schema.validateAndComputeTypePolicy(
typeDefinition: GQLTypeDefinition,
typePolicyCache: MutableMap<String, TypePolicy?>,
): TypePolicy? {
if (typePolicyCache.contains(typeDefinition.name)) {
return typePolicyCache[typeDefinition.name]
}
val (directives, interfaces) = when (typeDefinition) {
is GQLObjectTypeDefinition -> typeDefinition.directives to typeDefinition.implementsInterfaces
is GQLInterfaceTypeDefinition -> typeDefinition.directives to typeDefinition.implementsInterfaces
else -> error("Unexpected $typeDefinition")
}

val interfacesTypePolicy = interfaces.mapNotNull { validateAndComputeTypePolicy(typeDefinitions[it]!!, typePolicyCache) }
val distinct = interfacesTypePolicy.distinct()
if (distinct.size > 1) {
val extra = interfaces.indices.joinToString("\n") {
"${interfaces[it]}: ${interfacesTypePolicy[it]}"
}
throw SourceAwareException(
error = "Apollo: Type '${typeDefinition.name}' cannot inherit different @typePolicy from different interfaces:\n$extra",
sourceLocation = typeDefinition.sourceLocation
)
}

val typePolicyDirective = directives.firstOrNull { originalDirectiveName(it.name) == TYPE_POLICY }
val typePolicy = typePolicyDirective?.toTypePolicy()
val ret = if (typePolicy != null) {
if (distinct.isNotEmpty()) {
val extra = interfaces.indices.joinToString("\n") {
"${interfaces[it]}: ${interfacesTypePolicy[it]}"
}
throw SourceAwareException(
error = "Apollo: Type '${typeDefinition.name}' cannot have @typePolicy since it implements the following interfaces which also have @typePolicy: $extra",
sourceLocation = typeDefinition.sourceLocation
)
}
typePolicy
} else {
distinct.firstOrNull()
}
typePolicyCache[typeDefinition.name] = ret
return ret
}

private fun GQLDirective.toTypePolicy(): TypePolicy {
val keyFields = ((arguments.single { it.name == "keyFields" }.value as GQLStringValue).value
.parseAsGQLSelections().value?.map { gqlSelection ->
(gqlSelection as GQLField).name
} ?: throw SourceAwareException("Apollo: keyArgs should be a selectionSet", sourceLocation))
.toSet()

return TypePolicy(
keyFields = keyFields,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class GetMaxAgesTest {
.validateAsSchema(
SchemaValidationOptions(
addKotlinLabsDefinitions = true,
foreignSchemas = builtinForeignSchemas() + ForeignSchema("cache", "v0.1", cacheControlGQLDefinitions)
foreignSchemas = builtinForeignSchemas() + ForeignSchema("cache", "v0.1", cacheGQLDefinitions)
)
)
.getOrThrow()
Expand Down Expand Up @@ -147,7 +147,7 @@ class GetMaxAgesTest {
.validateAsSchema(
SchemaValidationOptions(
addKotlinLabsDefinitions = true,
foreignSchemas = builtinForeignSchemas() + ForeignSchema("cache", "v0.1", cacheControlGQLDefinitions)
foreignSchemas = builtinForeignSchemas() + ForeignSchema("cache", "v0.1", cacheGQLDefinitions)
)
)
.getOrThrow()
Expand Down
Loading