Skip to content
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
PUT_CHANGELOG_HERE

- Database files are now stored in the `cache` directory by default on Android (#164)
- `FieldPolicyCacheResolver` now uses the `fieldPolicies` passed to its constructor instead of the information from `CompiledField`. This allows to better isolate the main Apollo Kotlin project from the cache project. (#250)
If your code was calling `FieldPolicyCacheResolver()` directly, update it to pass `Cache.fieldPolicies` to the constructor.
- Rename `IdCacheKeyResolver` to `IdCacheResolver`, and keep the list ids and single id arguments in the same list. (#250)

# v1.0.0-alpha.7
_2025-10-14_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran
)
}

private fun GQLOperationDefinition.withRequiredFields(schema: Schema, keyFields: Map<String, Set<String>>): GQLOperationDefinition {
private fun GQLOperationDefinition.withRequiredFields(schema: Schema, keyFields: Map<String, List<String>>): GQLOperationDefinition {
val parentType = rootTypeDefinition(schema)!!.name
return copy(
selections = selections.withRequiredFields(
Expand All @@ -55,7 +55,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran
)
}

private fun GQLFragmentDefinition.withRequiredFields(schema: Schema, keyFields: Map<String, Set<String>>): GQLFragmentDefinition {
private fun GQLFragmentDefinition.withRequiredFields(schema: Schema, keyFields: Map<String, List<String>>): GQLFragmentDefinition {
return copy(
selections = selections.withRequiredFields(
schema = schema,
Expand All @@ -73,7 +73,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran
@OptIn(ApolloInternal::class)
private fun List<GQLSelection>.withRequiredFields(
schema: Schema,
keyFields: Map<String, Set<String>>,
keyFields: Map<String, List<String>>,
parentType: String,
isRoot: Boolean,
): List<GQLSelection> {
Expand Down Expand Up @@ -106,7 +106,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran
return newSelections
}

val parentTypeKeyFields = keyFields[parentType] ?: emptySet()
val parentTypeKeyFields = keyFields[parentType] ?: emptyList()
newSelections.filterIsInstance<GQLField>().forEach {
// Disallow fields whose alias conflicts with a key field, or is "__typename"
if (parentTypeKeyFields.contains(it.alias) || it.alias == "__typename") {
Expand All @@ -118,7 +118,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran
}

// Add key fields
val fieldNames = newSelections.filterIsInstance<GQLField>().map { it.responseName() }
val fieldNames = newSelections.filterIsInstance<GQLField>().map { it.responseName() }.toSet()
val fieldNamesToAdd = (parentTypeKeyFields - fieldNames)

// Unions and interfaces without key fields: add key fields of all possible types in inline fragments
Expand All @@ -130,7 +130,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran
emptySet()
}
possibleTypes
.associateWith { possibleType -> keyFields[possibleType] ?: emptySet() }
.associateWith { possibleType -> keyFields[possibleType] ?: emptyList() }
.mapNotNull { (possibleType, possibleTypeKeyFields) ->
val fieldNamesToAddInInlineFragment = possibleTypeKeyFields - fieldNames
if (fieldNamesToAddInInlineFragment.isNotEmpty()) {
Expand All @@ -156,7 +156,7 @@ internal object AddKeyFieldsExecutableDocumentTransform : ExecutableDocumentTran

private fun GQLField.withRequiredFields(
schema: Schema,
keyFields: Map<String, Set<String>>,
keyFields: Map<String, List<String>>,
parentType: String,
): GQLField {
val typeDefinition = definitionFromScope(schema, parentType)!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ private object Symbols {
val Seconds = MemberName(Duration.Companion::class.asTypeName(), "seconds", isExtension = true)
val TypePolicy = ClassName("com.apollographql.cache.normalized.api", "TypePolicy")
val EmbeddedFields = ClassName("com.apollographql.cache.normalized.api", "EmbeddedFields")
val FieldPolicies = ClassName("com.apollographql.cache.normalized.api", "FieldPolicies")
val FieldPolicy = FieldPolicies.nestedClass("FieldPolicy")
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")
Expand All @@ -59,13 +61,15 @@ internal class CacheSchemaCodeGenerator(
?: environment.arguments["packageName"] as? String
?: throw IllegalArgumentException("com.apollographql.cache.packageName argument is required and must be a String")) + ".cache"
val typePolicies = validSchema.getTypePolicies()
val fieldPolicies = validSchema.getFieldPolicies()
val connectionTypes = validSchema.getConnectionTypes()
val embeddedFields = validSchema.getEmbeddedFields(typePolicies, connectionTypes)
val file = FileSpec.builder(packageName, "Cache")
.addType(
TypeSpec.objectBuilder("Cache")
.addProperty(maxAgeProperty(validSchema))
.addProperty(typePoliciesProperty(typePolicies))
.addProperty(fieldPoliciesProperty(fieldPolicies))
.addProperty(embeddedFieldsProperty(embeddedFields))
.addProperty(connectionTypesProperty(connectionTypes))
.addFunction(cacheFunction())
Expand Down Expand Up @@ -145,7 +149,7 @@ internal class CacheSchemaCodeGenerator(
typePolicies.forEach { (type, typePolicy) ->
addStatement("%S to %T(", type, Symbols.TypePolicy)
withIndent {
addStatement("keyFields = setOf(")
addStatement("keyFields = listOf(")
withIndent {
typePolicy.keyFields.forEach { keyField ->
addStatement("%S, ", keyField)
Expand Down Expand Up @@ -191,6 +195,49 @@ internal class CacheSchemaCodeGenerator(
.build()
}

private fun fieldPoliciesProperty(fieldPolicies: Map<String, FieldPolicies>): PropertySpec {
val initializer = if (fieldPolicies.isEmpty()) {
CodeBlock.of("emptyMap()")
} else {
CodeBlock.builder().apply {
add("mapOf(\n")
withIndent {
fieldPolicies.forEach { (type, fieldPolicies) ->
addStatement("%S to %T(", type, Symbols.FieldPolicies)
withIndent {
addStatement("fieldPolicies = mapOf(")
withIndent {
fieldPolicies.fieldPolicies.forEach { (fieldName, fieldPolicy) ->
addStatement("%S to %T(", fieldName, Symbols.FieldPolicy)
withIndent {
addStatement("keyArgs = listOf(")
fieldPolicy.keyArgs.forEach { keyArg ->
withIndent {
addStatement("%S, ", keyArg)
}
}
add("),\n")
}
add("),\n")
}
}
add("),\n")
}
addStatement("),")
}
}
add(")")
}
.build()
}
return PropertySpec.builder(
name = "fieldPolicies",
type = MAP.parameterizedBy(STRING, Symbols.FieldPolicies)
)
.initializer(initializer)
.build()
}

private fun embeddedFieldsProperty(embeddedFields: Map<String, EmbeddedFields>): PropertySpec {
val initializer = if (embeddedFields.isEmpty()) {
CodeBlock.of("emptyMap()")
Expand All @@ -201,7 +248,7 @@ internal class CacheSchemaCodeGenerator(
embeddedFields.forEach { (type, embeddedField) ->
addStatement("%S to %T(", type, Symbols.EmbeddedFields)
withIndent {
addStatement("embeddedFields = setOf(")
addStatement("embeddedFields = listOf(")
withIndent {
embeddedField.embeddedFields.forEach { embeddedField ->
addStatement("%S, ", embeddedField)
Expand Down Expand Up @@ -250,6 +297,7 @@ internal class CacheSchemaCodeGenerator(
"return %M(\n⇥" +
"normalizedCacheFactory = normalizedCacheFactory,\n" +
"typePolicies = typePolicies,\n" +
"fieldPolicies = fieldPolicies,\n" +
"connectionTypes = connectionTypes, \n" +
"embeddedFields = embeddedFields, \n" +
"maxAges = maxAges,\n" +
Expand All @@ -260,7 +308,7 @@ internal class CacheSchemaCodeGenerator(
")",
Symbols.NormalisedCacheExtension,
)
.build()
.build(),
)
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ internal val GQLTypeDefinition.fields
else -> emptyList()
}

internal fun GQLDirective.extractFields(argumentName: String): Set<String> {
internal fun GQLDirective.extractFields(argumentName: String): List<String> {
return (((arguments.singleOrNull { it.name == argumentName }?.value as? GQLStringValue)?.value ?: "")
.parseAsGQLSelections().value?.map { gqlSelection ->
if (gqlSelection !is GQLField) {
throw SourceAwareException("Apollo: $argumentName values should be field selections", sourceLocation)
throw SourceAwareException("Apollo: $argumentName values must be field selections", sourceLocation)
}
if (gqlSelection.selections.isNotEmpty()) {
throw SourceAwareException("Apollo: composite fields are not supported in $argumentName", sourceLocation)
}
gqlSelection.name
} ?: throw SourceAwareException("Apollo: $argumentName should be a selectionSet", sourceLocation))
.toSet()
} ?: throw SourceAwareException("Apollo: $argumentName must be a selectionSet", sourceLocation))
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@ import com.apollographql.apollo.ast.Schema
import com.apollographql.apollo.ast.rawType

internal data class EmbeddedFields(
val embeddedFields: Set<String>,
val embeddedFields: List<String>,
)

internal fun Schema.getEmbeddedFields(
typePolicies: Map<String, TypePolicy>,
connectionTypes: Set<String>,
): Map<String, EmbeddedFields> {
// Fields manually specified as embedded
val embeddedFields: Map<String, Set<String>> = typePolicies
val embeddedFields: Map<String, List<String>> = typePolicies
.filter { it.value.embeddedFields.isNotEmpty() }
.mapValues { it.value.embeddedFields }
// Fields that are of a connection type
val connectionFields: Map<String, Set<String>> = getConnectionFields(connectionTypes)
val connectionFields: Map<String, List<String>> = getConnectionFields(connectionTypes)
// Specific Connection type fields
val connectionTypeFields: Map<String, Set<String>> = connectionTypes.associateWith { setOf("edges", "pageInfo") }
val connectionTypeFields: Map<String, List<String>> = connectionTypes.associateWith { listOf("edges", "pageInfo") }
// Merge all
return (embeddedFields.entries + connectionFields.entries + connectionTypeFields.entries)
.groupBy({ it.key }, { it.value })
.mapValues { entry -> EmbeddedFields(entry.value.flatten().toSet()) }
.mapValues { entry -> EmbeddedFields(entry.value.flatten().distinct()) }
}

private fun Schema.getConnectionFields(connectionTypes: Set<String>): Map<String, Set<String>> {
private fun Schema.getConnectionFields(connectionTypes: Set<String>): Map<String, List<String>> {
return typeDefinitions.values
.filter { it is GQLObjectTypeDefinition || it is GQLInterfaceTypeDefinition }
.mapNotNull { typeDefinition ->
Expand All @@ -38,7 +38,7 @@ private fun Schema.getConnectionFields(connectionTypes: Set<String>): Map<String
} else {
null
}
}.toSet()
}
if (connectionFields.isNotEmpty()) {
typeDefinition.name to connectionFields
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.apollographql.cache.apollocompilerplugin.internal

import com.apollographql.apollo.ast.GQLDirective
import com.apollographql.apollo.ast.GQLObjectTypeDefinition
import com.apollographql.apollo.ast.GQLStringValue
import com.apollographql.apollo.ast.Schema
import com.apollographql.apollo.ast.Schema.Companion.FIELD_POLICY
import com.apollographql.apollo.ast.SourceAwareException
import com.apollographql.cache.apollocompilerplugin.internal.FieldPolicies.FieldPolicy

internal data class FieldPolicies(
val fieldPolicies: Map<String, FieldPolicy>,
) {
internal data class FieldPolicy(
val keyArgs: List<String>,
)
}

/**
* Returns the field policies for object types in the schema.
*/
internal fun Schema.getFieldPolicies(): Map<String, FieldPolicies> {
@Suppress("UNCHECKED_CAST")
return typeDefinitions.values
.filterIsInstance<GQLObjectTypeDefinition>()
.associate {
it.name to validateAndComputeFieldPolicies(it)
}
.filterValues { it != null }
.mapValues { FieldPolicies(it.value!!) }
}

private fun Schema.validateAndComputeFieldPolicies(typeDefinition: GQLObjectTypeDefinition): Map<String, FieldPolicy>? {
val fieldPolicyDirectives = typeDefinition.directives.filter { originalDirectiveName(it.name) == FIELD_POLICY }.ifEmpty { return null }
val fieldPolicies = fieldPolicyDirectives.associate { it.toFieldPolicy() }
for ((fieldName, keyArgs) in fieldPolicies) {
val field = typeDefinition.fields.singleOrNull { it.name == fieldName }
if (field == null) {
throw SourceAwareException("Apollo: unknown field '${typeDefinition.name}.$fieldName' in @fieldPolicy", typeDefinition.sourceLocation)
}
for (keyArg in keyArgs.keyArgs) {
if (field.arguments.none { it.name == keyArg }) {
throw SourceAwareException("Apollo: unknown argument '${typeDefinition.name}.$fieldName($keyArg:)' in @fieldPolicy", typeDefinition.sourceLocation)
}
}
}
return fieldPolicies
}

private fun GQLDirective.toFieldPolicy(): Pair<String, FieldPolicy> {
val forField = (arguments.singleOrNull { it.name == "forField" }?.value as? GQLStringValue)?.value
?: throw SourceAwareException("Apollo: missing or wrong type 'forField' argument in @fieldPolicy", sourceLocation)
return forField to FieldPolicy(extractFields("keyArgs"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import com.apollographql.apollo.ast.Schema.Companion.TYPE_POLICY
import com.apollographql.apollo.ast.SourceAwareException

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

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@file:OptIn(ApolloExperimental::class)
@file:Suppress("ApolloMissingGraphQLDefinitionImport", "GraphQLUnresolvedReference")

package com.apollographql.cache.apollocompilerplugin.internal

Expand Down Expand Up @@ -71,7 +72,7 @@ class AddKeyFieldsExecutableDocumentTransformTest {
interface HasID @typePolicy(keyFields: "id") {
id: ID!
}

interface User implements HasID {
id: ID!
name: String!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@file:OptIn(ApolloExperimental::class)
@file:Suppress("ApolloMissingGraphQLDefinitionImport", "GraphQLUnresolvedReference")

package com.apollographql.cache.apollocompilerplugin.internal

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@file:OptIn(ApolloExperimental::class)
@file:Suppress("ApolloMissingGraphQLDefinitionImport", "GraphQLUnresolvedReference")

package com.apollographql.cache.apollocompilerplugin.internal

Expand Down Expand Up @@ -72,9 +73,9 @@ class GetEmbeddedFieldsTest {
val connectionTypes = schema.getConnectionTypes()
val embeddedFields = schema.getEmbeddedFields(typePolicies, connectionTypes)
val expected = mapOf(
"Query" to EmbeddedFields(setOf("users")),
"User" to EmbeddedFields(setOf("name", "email")),
"UserConnection" to EmbeddedFields(setOf("edges", "pageInfo")),
"Query" to EmbeddedFields(listOf("users")),
"User" to EmbeddedFields(listOf("name", "email")),
"UserConnection" to EmbeddedFields(listOf("edges", "pageInfo")),
)
assertEquals(expected, embeddedFields)
}
Expand Down Expand Up @@ -102,9 +103,9 @@ class GetEmbeddedFieldsTest {
val connectionTypes = schema.getConnectionTypes()
val embeddedFields = schema.getEmbeddedFields(typePolicies, connectionTypes)
val expected = mapOf(
"Query" to EmbeddedFields(setOf("users")),
"User" to EmbeddedFields(setOf("name", "email")),
"UserConnection" to EmbeddedFields(setOf("edges", "pageInfo")),
"Query" to EmbeddedFields(listOf("users")),
"User" to EmbeddedFields(listOf("name", "email")),
"UserConnection" to EmbeddedFields(listOf("edges", "pageInfo")),
)
assertEquals(expected, embeddedFields)
}
Expand Down
Loading