Skip to content

Commit 67db31e

Browse files
authored
Generate connectionTypes and embeddedFields in Cache object. (#230)
* Generate connectionTypes and embeddedFields in Cache object. * Make TypePolicy and EmbeddedFields inline value classes. * Doc: remove mention of `paginationArgs` (deprecated) and use `FieldKeyGenerator` instead * Revert bad .md change * Formatting * Fix test * Fix tests * Update Changelog
1 parent f97ba93 commit 67db31e

File tree

45 files changed

+871
-292
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+871
-292
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
PUT_CHANGELOG_HERE
44

5+
- Pagination API tweaks (#230)
6+
- Configuring connection fields with `@typePolicy(connectionFields: "...")` is deprecated. Instead, apply `@connection` to the connection types.
7+
- Configuring pagination arguments with `@fieldPolicy(forField: "...", paginationArgs: "...")` is deprecated. Instead, configure a `FieldKeyGenerator` on your cache.
8+
- The API of `EmbeddedFieldsProvider` has been tweaked to allow determining if fields should be embedded field by field, rather than all at once based on the type.
9+
510
# v1.0.0-alpha.6
611
_2025-08-21_
712

Writerside/topics/expiration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ To declare the maximum age of types and fields in the schema, use the `@cacheCon
8989
```
9090
# First import the directives
9191
extend schema @link(
92-
url: "https://specs.apollo.dev/cache/v0.1",
92+
url: "https://specs.apollo.dev/cache/v0.3",
9393
import: ["@cacheControl", "@cacheControlField"]
9494
)
9595
@@ -113,5 +113,5 @@ cacheResolver = CacheControlCacheResolver(
113113
maxAges = Cache.maxAges,
114114
defaultMaxAge = 1.hours,
115115
)
116-
),
116+
)
117117
```

Writerside/topics/pagination/pagination-other.md

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,36 @@ with more configuration needed.
55

66
#### Pagination arguments
77

8-
The `@fieldPolicy` directive has a `paginationArgs` argument that can be used to specify the arguments that should be omitted from the field key.
8+
Arguments that should be omitted from the field key can be specified programmatically by configuring your cache with a [`FieldKeyGenerator`](https://apollographql.github.io/apollo-kotlin-normalized-cache/kdoc/normalized-cache/com.apollographql.cache.normalized.api/-field-key-generator/index.html?query=interface%20FieldKeyGenerator) implementation:
99

10-
Going back to [the example](pagination-home.md) with `usersPage`:
11-
12-
13-
```graphql
14-
extend type Query
15-
@fieldPolicy(forField: "usersPage" paginationArgs: "page")
10+
```kotlin
11+
object MyFieldKeyGenerator : FieldKeyGenerator {
12+
override fun getFieldKey(context: FieldKeyContext): String {
13+
return if (context.parentType == "Query" && context.field.name == "usersPage") {
14+
context.field.newBuilder()
15+
.arguments(
16+
context.field.arguments.filter { argument ->
17+
argument.definition.name != "page" // Omit the `page` argument from the field key
18+
}
19+
)
20+
.build()
21+
.nameWithArguments(context.variables)
22+
} else {
23+
DefaultFieldKeyGenerator.getFieldKey(context)
24+
}
25+
}
26+
}
1627
```
1728

18-
> This can also be done programmatically by configuring your cache with a [`FieldKeyGenerator`](https://apollographql.github.io/apollo-kotlin-normalized-cache/kdoc/normalized-cache/com.apollographql.cache.normalized.api/-field-key-generator/index.html?query=interface%20FieldKeyGenerator) implementation.
29+
```kotlin
30+
val client = ApolloClient.Builder()
31+
// ...
32+
.normalizedCache(
33+
normalizedCacheFactory = cacheFactory,
34+
fieldKeyGenerator = MyFieldKeyGenerator, // Configure the cache with the custom field key generator
35+
)
36+
.build()
37+
```
1938

2039
With that in place, after fetching the first page, the cache will look like this:
2140

@@ -91,11 +110,25 @@ Fields in records can have arbitrary metadata attached to them, in addition to t
91110

92111
Let's go back to the [example](pagination-relay-style.md) where Relay-style pagination is used.
93112

94-
Configure the `paginationArgs` as seen previously:
113+
Configure the `fieldKeyGenerator` as seen previously:
95114

96-
```graphql
97-
extend type Query
98-
@fieldPolicy(forField: "usersConnection" paginationArgs: "first,after,last,before")
115+
```kotlin
116+
object MyFieldKeyGenerator : FieldKeyGenerator {
117+
override fun getFieldKey(context: FieldKeyContext): String {
118+
return if (context.field.type.rawType().name == "UserConnection") {
119+
context.field.newBuilder()
120+
.arguments(
121+
context.field.arguments.filter { argument ->
122+
argument.definition.name !in setOf("first", "after", "last", "before") // Omit pagination arguments from the field key
123+
}
124+
)
125+
.build()
126+
.nameWithArguments(context.variables)
127+
} else {
128+
DefaultFieldKeyGenerator.getFieldKey(context)
129+
}
130+
}
131+
}
99132
```
100133

101134
Now let's store in the metadata of each `UserConnection` field the values of the `before` and `after` arguments of the field returning it,

Writerside/topics/pagination/pagination-relay-style.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,25 @@ query UsersConnection($first: Int, $after: String, $last: Int, $before: String)
4848
}
4949
```
5050

51-
If your schema uses this pagination style, the library supports it out of the box: use the `connectionFields` argument to specify the fields
52-
that return a connection:
51+
If your schema uses this pagination style, the library supports it out of the box: use the `@connection` directive on Connection types:
5352

5453
```graphql
55-
extend type Query @typePolicy(connectionFields: "usersConnection")
54+
# First import the directive
55+
extend schema @link(
56+
url: "https://specs.apollo.dev/cache/v0.3",
57+
import: ["@connection"]
58+
)
59+
60+
# Then extend your types
61+
extend type UserConnection @connection
5662
```
5763

58-
In Kotlin, configure the cache like this, using the generated `Pagination` object:
64+
In Kotlin, configure the cache like this, using the generated `cache()` function:
5965

6066
```kotlin
6167
val client = ApolloClient.Builder()
6268
// ...
63-
.normalizedCache(
64-
normalizedCacheFactory = cacheFactory,
65-
metadataGenerator = ConnectionMetadataGenerator(Pagination.connectionTypes),
66-
recordMerger = ConnectionRecordMerger
67-
)
69+
.cache(cacheFactory)
6870
.build()
6971
```
7072

normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/ApolloCacheCompilerPlugin.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@
33
package com.apollographql.cache.apollocompilerplugin
44

55
import com.apollographql.apollo.annotations.ApolloExperimental
6-
import com.apollographql.apollo.ast.ForeignSchema
76
import com.apollographql.apollo.compiler.ApolloCompilerPlugin
87
import com.apollographql.apollo.compiler.ApolloCompilerPluginEnvironment
98
import com.apollographql.apollo.compiler.ApolloCompilerRegistry
109
import com.apollographql.cache.apollocompilerplugin.internal.AddKeyFieldsExecutableDocumentTransform
1110
import com.apollographql.cache.apollocompilerplugin.internal.CacheSchemaCodeGenerator
12-
import com.apollographql.cache.apollocompilerplugin.internal.cacheGQLDefinitions
11+
import com.apollographql.cache.apollocompilerplugin.internal.cacheForeignSchema
1312

1413
class ApolloCacheCompilerPlugin : ApolloCompilerPlugin {
1514
override fun beforeCompilationStep(
1615
environment: ApolloCompilerPluginEnvironment,
1716
registry: ApolloCompilerRegistry,
1817
) {
19-
registry.registerForeignSchemas(listOf(ForeignSchema("cache", "v0.1", cacheGQLDefinitions)))
18+
registry.registerForeignSchemas(listOf(cacheForeignSchema))
2019
registry.registerExecutableDocumentTransform("com.apollographql.cache.addKeyFields", transform = AddKeyFieldsExecutableDocumentTransform)
2120
registry.registerSchemaCodeGenerator(CacheSchemaCodeGenerator(environment))
2221
}

normalized-cache-apollo-compiler-plugin/src/main/kotlin/com/apollographql/cache/apollocompilerplugin/internal/CacheSchemaCodeGenerator.kt

Lines changed: 91 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ package com.apollographql.cache.apollocompilerplugin.internal
44

55
import com.apollographql.apollo.annotations.ApolloExperimental
66
import com.apollographql.apollo.ast.GQLDocument
7-
import com.apollographql.apollo.ast.GQLInterfaceTypeDefinition
8-
import com.apollographql.apollo.ast.GQLObjectTypeDefinition
9-
import com.apollographql.apollo.ast.GQLStringValue
10-
import com.apollographql.apollo.ast.GQLUnionTypeDefinition
117
import com.apollographql.apollo.ast.Schema
12-
import com.apollographql.apollo.ast.Schema.Companion.TYPE_POLICY
138
import com.apollographql.apollo.ast.toSchema
149
import com.apollographql.apollo.compiler.ApolloCompiler
1510
import com.apollographql.apollo.compiler.ApolloCompilerPluginEnvironment
@@ -40,6 +35,7 @@ private object Symbols {
4035
val MaxAgeDuration = MaxAge.nestedClass("Duration")
4136
val Seconds = MemberName(Duration.Companion::class.asTypeName(), "seconds", isExtension = true)
4237
val TypePolicy = ClassName("com.apollographql.cache.normalized.api", "TypePolicy")
38+
val EmbeddedFields = ClassName("com.apollographql.cache.normalized.api", "EmbeddedFields")
4339
val ApolloClientBuilder = ClassName("com.apollographql.apollo", "ApolloClient", "Builder")
4440
val NormalizedCacheFactory = ClassName("com.apollographql.cache.normalized.api", "NormalizedCacheFactory")
4541
val CacheKeyScope = ClassName("com.apollographql.cache.normalized.api", "CacheKey", "Scope")
@@ -59,16 +55,20 @@ internal class CacheSchemaCodeGenerator(
5955
}
6056
val packageName = (
6157
environment.arguments["com.apollographql.cache.packageName"] as? String
62-
// Fallback to the old argument name for compatibility with pre-v1.0.0 versions
58+
// Fallback to the old argument name for compatibility with pre-v1.0.0 versions
6359
?: environment.arguments["packageName"] as? String
6460
?: throw IllegalArgumentException("com.apollographql.cache.packageName argument is required and must be a String")) + ".cache"
61+
val typePolicies = validSchema.getTypePolicies()
62+
val connectionTypes = validSchema.getConnectionTypes()
63+
val embeddedFields = validSchema.getEmbeddedFields(typePolicies, connectionTypes)
6564
val file = FileSpec.builder(packageName, "Cache")
6665
.addType(
6766
TypeSpec.objectBuilder("Cache")
6867
.addProperty(maxAgeProperty(validSchema))
69-
.addProperty(typePoliciesProperty(validSchema))
70-
.addProperty(connectionTypesProperty(validSchema, packageName))
71-
.addFunction(cacheFunction(validSchema))
68+
.addProperty(typePoliciesProperty(typePolicies))
69+
.addProperty(embeddedFieldsProperty(embeddedFields))
70+
.addProperty(connectionTypesProperty(connectionTypes))
71+
.addFunction(cacheFunction())
7272
.build()
7373
)
7474
.addFileComment(
@@ -108,50 +108,58 @@ internal class CacheSchemaCodeGenerator(
108108

109109
private fun maxAgeProperty(schema: Schema): PropertySpec {
110110
val maxAges = schema.getMaxAges(environment.logger())
111-
val initializer = CodeBlock.builder().apply {
112-
add("mapOf(\n")
113-
withIndent {
114-
maxAges.forEach { (field, duration) ->
115-
if (duration == -1) {
116-
addStatement("%S to %T,", field, Symbols.MaxAgeInherit)
117-
} else {
118-
addStatement("%S to %T(%L.%M),", field, Symbols.MaxAgeDuration, duration, Symbols.Seconds)
111+
val initializer = if (maxAges.isEmpty()) {
112+
CodeBlock.of("emptyMap()")
113+
} else {
114+
CodeBlock.builder().apply {
115+
add("mapOf(\n")
116+
withIndent {
117+
maxAges.forEach { (field, duration) ->
118+
if (duration == -1) {
119+
addStatement("%S to %T,", field, Symbols.MaxAgeInherit)
120+
} else {
121+
addStatement("%S to %T(%L.%M),", field, Symbols.MaxAgeDuration, duration, Symbols.Seconds)
122+
}
119123
}
120124
}
125+
add(")")
121126
}
122-
add(")")
127+
.build()
123128
}
124-
.build()
125-
return PropertySpec.Companion.builder(
129+
return PropertySpec.builder(
126130
name = "maxAges",
127131
type = MAP.parameterizedBy(STRING, Symbols.MaxAge)
128132
)
129133
.initializer(initializer)
130134
.build()
131135
}
132136

133-
private fun typePoliciesProperty(schema: Schema): PropertySpec {
134-
val typePolicies = schema.getTypePolicies()
135-
val initializer = CodeBlock.builder().apply {
136-
add("mapOf(\n")
137-
withIndent {
138-
typePolicies.forEach { (type, typePolicy) ->
139-
addStatement("%S to %T(", type, Symbols.TypePolicy)
140-
withIndent {
141-
addStatement("keyFields = setOf(")
137+
private fun typePoliciesProperty(typePolicies: Map<String, TypePolicy>): PropertySpec {
138+
val typePolicies = typePolicies.filter { it.value.keyFields.isNotEmpty() }
139+
val initializer = if (typePolicies.isEmpty()) {
140+
CodeBlock.of("emptyMap()")
141+
} else {
142+
CodeBlock.builder().apply {
143+
add("mapOf(\n")
144+
withIndent {
145+
typePolicies.forEach { (type, typePolicy) ->
146+
addStatement("%S to %T(", type, Symbols.TypePolicy)
142147
withIndent {
143-
typePolicy.keyFields.forEach { keyField ->
144-
addStatement("%S, ", keyField)
148+
addStatement("keyFields = setOf(")
149+
withIndent {
150+
typePolicy.keyFields.forEach { keyField ->
151+
addStatement("%S, ", keyField)
152+
}
145153
}
154+
add("),\n")
146155
}
147-
add("),\n")
156+
addStatement("),")
148157
}
149-
addStatement("),")
150158
}
159+
add(")")
151160
}
152-
add(")")
161+
.build()
153162
}
154-
.build()
155163
return PropertySpec.builder(
156164
name = "typePolicies",
157165
type = MAP.parameterizedBy(STRING, Symbols.TypePolicy)
@@ -160,14 +168,20 @@ internal class CacheSchemaCodeGenerator(
160168
.build()
161169
}
162170

163-
private fun connectionTypesProperty(schema: Schema, packageName: String): PropertySpec {
164-
// TODO: connectionTypes is generated by the Apollo compiler for now, and we just reference it. Instead we should generate it here.
165-
val hasPagination = schema.hasConnectionFields()
166-
val initializer = if (hasPagination) {
167-
val paginationPackageName = packageName.substringBeforeLast(".") + ".pagination"
168-
CodeBlock.of("%T.connectionTypes", ClassName(paginationPackageName, "Pagination"))
169-
} else {
171+
private fun connectionTypesProperty(connectionTypes: Set<String>): PropertySpec {
172+
val initializer = if (connectionTypes.isEmpty()) {
170173
CodeBlock.of("emptySet()")
174+
} else {
175+
CodeBlock.builder().apply {
176+
add("setOf(\n")
177+
withIndent {
178+
connectionTypes.forEach { connectionType ->
179+
addStatement("%S,", connectionType)
180+
}
181+
}
182+
add(")")
183+
}
184+
.build()
171185
}
172186
return PropertySpec.builder(
173187
name = "connectionTypes",
@@ -177,8 +191,40 @@ internal class CacheSchemaCodeGenerator(
177191
.build()
178192
}
179193

180-
private fun cacheFunction(validSchema: Schema): FunSpec {
181-
validSchema.hasConnectionFields()
194+
private fun embeddedFieldsProperty(embeddedFields: Map<String, EmbeddedFields>): PropertySpec {
195+
val initializer = if (embeddedFields.isEmpty()) {
196+
CodeBlock.of("emptyMap()")
197+
} else {
198+
CodeBlock.builder().apply {
199+
add("mapOf(\n")
200+
withIndent {
201+
embeddedFields.forEach { (type, embeddedField) ->
202+
addStatement("%S to %T(", type, Symbols.EmbeddedFields)
203+
withIndent {
204+
addStatement("embeddedFields = setOf(")
205+
withIndent {
206+
embeddedField.embeddedFields.forEach { embeddedField ->
207+
addStatement("%S, ", embeddedField)
208+
}
209+
}
210+
add("),\n")
211+
}
212+
addStatement("),")
213+
}
214+
}
215+
add(")")
216+
}
217+
.build()
218+
}
219+
return PropertySpec.builder(
220+
name = "embeddedFields",
221+
type = MAP.parameterizedBy(STRING, Symbols.EmbeddedFields)
222+
)
223+
.initializer(initializer)
224+
.build()
225+
}
226+
227+
private fun cacheFunction(): FunSpec {
182228
return FunSpec.builder("cache")
183229
.receiver(Symbols.ApolloClientBuilder)
184230
.addParameter("normalizedCacheFactory", Symbols.NormalizedCacheFactory)
@@ -205,6 +251,7 @@ internal class CacheSchemaCodeGenerator(
205251
"normalizedCacheFactory = normalizedCacheFactory,\n" +
206252
"typePolicies = typePolicies,\n" +
207253
"connectionTypes = connectionTypes, \n" +
254+
"embeddedFields = embeddedFields, \n" +
208255
"maxAges = maxAges,\n" +
209256
"defaultMaxAge = defaultMaxAge,\n" +
210257
"keyScope = keyScope,\n" +
@@ -217,16 +264,4 @@ internal class CacheSchemaCodeGenerator(
217264
)
218265
.build()
219266
}
220-
221-
private fun Schema.hasConnectionFields(): Boolean {
222-
val directives = typeDefinitions.values.filterIsInstance<GQLObjectTypeDefinition>().flatMap { it.directives } +
223-
typeDefinitions.values.filterIsInstance<GQLInterfaceTypeDefinition>().flatMap { it.directives } +
224-
typeDefinitions.values.filterIsInstance<GQLUnionTypeDefinition>().flatMap { it.directives }
225-
return directives.any {
226-
originalDirectiveName(it.name) == TYPE_POLICY &&
227-
it.arguments.any { arg ->
228-
arg.name == "connectionFields" && !(arg.value as? GQLStringValue)?.value.isNullOrBlank()
229-
}
230-
}
231-
}
232267
}

0 commit comments

Comments
 (0)