Skip to content
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

Cache max age codegen #6132

Closed
wants to merge 2 commits into from
Closed
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 libraries/apollo-ast/api/apollo-ast.api
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,7 @@ public final class com/apollographql/apollo/ast/GqldirectiveKt {

public final class com/apollographql/apollo/ast/GqldocumentKt {
public static final fun builtinDefinitions ()Ljava/util/List;
public static final fun cacheDefinitions (Ljava/lang/String;)Ljava/util/List;
public static final fun kotlinLabsDefinitions (Ljava/lang/String;)Ljava/util/List;
public static final fun linkDefinitions ()Ljava/util/List;
public static final fun nullabilityDefinitions (Ljava/lang/String;)Ljava/util/List;
Expand Down Expand Up @@ -927,6 +928,8 @@ public final class com/apollographql/apollo/ast/ReservedEnumValueName : com/apol
}

public final class com/apollographql/apollo/ast/Schema {
public static final field CACHE_CONTROL Ljava/lang/String;
public static final field CACHE_CONTROL_FIELD Ljava/lang/String;
public static final field CATCH Ljava/lang/String;
public static final field CATCH_BY_DEFAULT Ljava/lang/String;
public static final field Companion Lcom/apollographql/apollo/ast/Schema$Companion;
Expand Down
1 change: 1 addition & 0 deletions libraries/apollo-ast/api/apollo-ast.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,7 @@ final fun (kotlin/String).com.apollographql.apollo.ast/toGQLValue(com.apollograp
final fun (okio/Path).com.apollographql.apollo.ast/parseAsGQLDocument(com.apollographql.apollo.ast/ParserOptions = ...): com.apollographql.apollo.ast/GQLResult<com.apollographql.apollo.ast/GQLDocument> // com.apollographql.apollo.ast/parseAsGQLDocument|[email protected](com.apollographql.apollo.ast.ParserOptions){}[0]
final fun (okio/Path).com.apollographql.apollo.ast/toGQLDocument(com.apollographql.apollo.ast/ParserOptions = ..., kotlin/Boolean = ...): com.apollographql.apollo.ast/GQLDocument // com.apollographql.apollo.ast/toGQLDocument|[email protected](com.apollographql.apollo.ast.ParserOptions;kotlin.Boolean){}[0]
final fun com.apollographql.apollo.ast/builtinDefinitions(): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/builtinDefinitions|builtinDefinitions(){}[0]
final fun com.apollographql.apollo.ast/cacheDefinitions(kotlin/String): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/cacheDefinitions|cacheDefinitions(kotlin.String){}[0]
final fun com.apollographql.apollo.ast/kotlinLabsDefinitions(kotlin/String): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/kotlinLabsDefinitions|kotlinLabsDefinitions(kotlin.String){}[0]
final fun com.apollographql.apollo.ast/linkDefinitions(): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/linkDefinitions|linkDefinitions(){}[0]
final fun com.apollographql.apollo.ast/nullabilityDefinitions(kotlin/String): kotlin.collections/List<com.apollographql.apollo.ast/GQLDefinition> // com.apollographql.apollo.ast/nullabilityDefinitions|nullabilityDefinitions(kotlin.String){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Schema internal constructor(
private val directivesToStrip: List<String>,
@ApolloInternal
val connectionTypes: Set<String>,
@ApolloInternal
val maxAges: Map<String, Int>,
) {
@ApolloInternal
val schemaDefinition: GQLSchemaDefinition? = definitions.filterIsInstance<GQLSchemaDefinition>().singleOrNull()
Expand Down Expand Up @@ -136,6 +138,7 @@ class Schema internal constructor(
"foreignNames" to foreignNames,
"directivesToStrip" to directivesToStrip,
"connectionTypes" to connectionTypes.toList().sorted(),
"maxAges" to maxAges,
)
}

Expand Down Expand Up @@ -218,6 +221,10 @@ class Schema internal constructor(
const val SEMANTIC_NON_NULL_FIELD = "semanticNonNullField"
@ApolloExperimental
const val LINK = "link"
@ApolloExperimental
const val CACHE_CONTROL = "cacheControl"
@ApolloExperimental
const val CACHE_CONTROL_FIELD = "cacheControlField"

const val FIELD_POLICY_FOR_FIELD = "forField"
const val FIELD_POLICY_KEY_ARGS = "keyArgs"
Expand All @@ -238,6 +245,7 @@ class Schema internal constructor(
foreignNames = map["foreignNames"]!! as Map<String, String>,
directivesToStrip = map["directivesToStrip"]!! as List<String>,
connectionTypes = (map["connectionTypes"]!! as List<String>).toSet(),
maxAges = map["maxAges"]!! as Map<String, Int>,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.apollographql.apollo.annotations.ApolloExperimental
import com.apollographql.apollo.annotations.ApolloInternal
import com.apollographql.apollo.ast.internal.ExtensionsMerger
import com.apollographql.apollo.ast.internal.builtinsDefinitionsStr
import com.apollographql.apollo.ast.internal.cacheDefinitionsStr
import com.apollographql.apollo.ast.internal.ensureSchemaDefinition
import com.apollographql.apollo.ast.internal.kotlinLabsDefinitions
import com.apollographql.apollo.ast.internal.linkDefinitionsStr
Expand Down Expand Up @@ -106,6 +107,19 @@ fun nullabilityDefinitions(version: String): List<GQLDefinition> {
})
}

private const val CACHE_VERSION = "v0.1"

/**
* Extra cache definitions from https://specs.apollo.dev/cache/<[version]>
*/
fun cacheDefinitions(version: String): List<GQLDefinition> {
return definitionsFromString(when (version) {
CACHE_VERSION -> cacheDefinitionsStr
else -> error("cache/$version definitions are not supported, please use $CACHE_VERSION")
}
)
}

private fun definitionsFromString(string: String): List<GQLDefinition> {
return string
.parseAsGQLDocument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.apollographql.apollo.ast.internal
import com.apollographql.apollo.annotations.ApolloInternal
import com.apollographql.apollo.ast.ConflictResolution
import com.apollographql.apollo.ast.DirectiveRedefinition
import com.apollographql.apollo.ast.GQLBooleanValue
import com.apollographql.apollo.ast.GQLDefinition
import com.apollographql.apollo.ast.GQLDirective
import com.apollographql.apollo.ast.GQLDirectiveDefinition
Expand All @@ -11,6 +12,7 @@ import com.apollographql.apollo.ast.GQLDocument
import com.apollographql.apollo.ast.GQLEnumTypeDefinition
import com.apollographql.apollo.ast.GQLField
import com.apollographql.apollo.ast.GQLInputObjectTypeDefinition
import com.apollographql.apollo.ast.GQLIntValue
import com.apollographql.apollo.ast.GQLInterfaceTypeDefinition
import com.apollographql.apollo.ast.GQLListValue
import com.apollographql.apollo.ast.GQLNamed
Expand All @@ -36,8 +38,11 @@ import com.apollographql.apollo.ast.NULLABILITY_VERSION
import com.apollographql.apollo.ast.NoQueryType
import com.apollographql.apollo.ast.OtherValidationIssue
import com.apollographql.apollo.ast.Schema
import com.apollographql.apollo.ast.Schema.Companion.CACHE_CONTROL
import com.apollographql.apollo.ast.Schema.Companion.CACHE_CONTROL_FIELD
import com.apollographql.apollo.ast.Schema.Companion.TYPE_POLICY
import com.apollographql.apollo.ast.builtinDefinitions
import com.apollographql.apollo.ast.cacheDefinitions
import com.apollographql.apollo.ast.canHaveKeyFields
import com.apollographql.apollo.ast.combineDefinitions
import com.apollographql.apollo.ast.findOneOf
Expand Down Expand Up @@ -214,6 +219,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi

val keyFields = mergedScope.validateAndComputeKeyFields()
val connectionTypes = mergedScope.computeConnectionTypes()
val maxAges = mergedScope.computeMaxAges()

return GQLResult(
Schema(
Expand All @@ -222,6 +228,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
foreignNames = foreignNames,
directivesToStrip = directivesToStrip,
connectionTypes = connectionTypes,
maxAges = maxAges,
),
issues
)
Expand Down Expand Up @@ -361,6 +368,7 @@ private fun List<GQLSchemaExtension>.getForeignSchemas(
val foreignDefinitions = when (foreignName) {
"kotlin_labs" -> kotlinLabsDefinitions(version)
"nullability" -> nullabilityDefinitions(version)
"cache" -> cacheDefinitions(version)
else -> null
}

Expand Down Expand Up @@ -516,7 +524,8 @@ private fun ValidationScope.validateCatch(schemaDefinition: GQLSchemaDefinition?
issues.add(OtherValidationIssue(
message = "Schemas that include nullability directives must opt-in a default CatchTo. Use `extend schema @catchByDefault(to: \$to)`",
sourceLocation = null
))
)
)
return
}

Expand All @@ -528,13 +537,15 @@ private fun ValidationScope.validateCatch(schemaDefinition: GQLSchemaDefinition?
issues.add(OtherValidationIssue(
message = "Schemas that include nullability directives must opt-in a default CatchTo. Use `extend schema @catchByDefault(to: \$to)`",
sourceLocation = schemaDefinition.sourceLocation
))
)
)
return
} else if (catches.size > 1) {
issues.add(OtherValidationIssue(
message = "There can be only one `@catch` directive on the schema definition",
sourceLocation = schemaDefinition.sourceLocation
))
)
)
return
}
}
Expand Down Expand Up @@ -674,6 +685,63 @@ internal fun ValidationScope.computeConnectionTypes(): Set<String> {
return connectionTypes
}

internal fun ValidationScope.computeMaxAges(): Map<String, Int> {
fun GQLDirective.maxAgeAndInherit(): Pair<Int?, Boolean> {
val maxAge = (arguments.firstOrNull { it.name == "maxAge" }?.value as? GQLIntValue)?.value?.toIntOrNull()
if (maxAge != null && maxAge < 0) {
registerIssue("`maxAge` must not be negative", sourceLocation)
return null to false
}
val inheritMaxAge = (arguments.firstOrNull { it.name == "inheritMaxAge" }?.value as? GQLBooleanValue)?.value == true
if (maxAge == null && !inheritMaxAge || maxAge != null && inheritMaxAge) {
registerIssue("`@$name` must either provide a `maxAge` or an `inheritMaxAge` set to true", sourceLocation)
return null to false
}
return maxAge to inheritMaxAge
}

val maxAges = mutableMapOf<String, Int>()
for (typeDefinition in typeDefinitions.values) {
val typeCacheControlDirective = typeDefinition.directives.firstOrNull { originalDirectiveName(it.name) == CACHE_CONTROL }
if (typeCacheControlDirective != null) {
val (maxAge, inheritMaxAge) = typeCacheControlDirective.maxAgeAndInherit()
if (maxAge != null) {
maxAges[typeDefinition.name] = maxAge
} else if (inheritMaxAge) {
maxAges[typeDefinition.name] = -1
}
}

val typeCacheControlFieldDirectives = typeDefinition.directives.filter { originalDirectiveName(it.name) == CACHE_CONTROL_FIELD }
for (fieldDirective in typeCacheControlFieldDirectives) {
val fieldName = (fieldDirective.arguments.first { it.name == "name" }.value as GQLStringValue).value
if (typeDefinition.fields.none { it.name == fieldName }) {
registerIssue("Field `$fieldName` does not exist on type `${typeDefinition.name}`", fieldDirective.sourceLocation)
continue
}
val (maxAge, inheritMaxAge) = fieldDirective.maxAgeAndInherit()
if (maxAge != null) {
maxAges["${typeDefinition.name}.$fieldName"] = maxAge
} else if (inheritMaxAge) {
maxAges["${typeDefinition.name}.$fieldName"] = -1
}
}

for (field in typeDefinition.fields) {
val fieldCacheControlDirective = field.directives.firstOrNull { originalDirectiveName(it.name) == CACHE_CONTROL }
if (fieldCacheControlDirective != null) {
val (maxAge, inheritMaxAge) = fieldCacheControlDirective.maxAgeAndInherit()
if (maxAge != null) {
maxAges["${typeDefinition.name}.${field.name}"] = maxAge
} else if (inheritMaxAge) {
maxAges["${typeDefinition.name}.${field.name}"] = -1
}
}
}
}
return maxAges
}

private val GQLTypeDefinition.fields
get() = when (this) {
is GQLObjectTypeDefinition -> fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,65 @@ enum CatchTo {
THROW
}
""".trimIndent()

internal val cacheDefinitionsStr = """
""${'"'}
Possible values for the `@cacheControl` `scope` argument (unused on the client).
""${'"'}
enum CacheControlScope {
PUBLIC
PRIVATE
}

""${'"'}
Configures cache settings for a field or type.

- `maxAge`: The maximum amount of time the field's cached value is valid, in seconds.
- `inheritMaxAge`: If true, the field inherits the `maxAge` of its parent field. If set to `true`, `maxAge` must not be provided.
- `scope`: Unused on the client.

When applied to a type, the settings apply to all schema fields that return this type.
Field-level settings override type-level settings.

For example:

```graphql
type Query {
me: User
user(id: ID!): User @cacheControl(maxAge: 5)
}

type User @cacheControl(maxAge: 10) {
id: ID!
email: String
}
```
`Query.me` is valid for 10 seconds, and `Query.user` for 5 seconds.
""${'"'}
directive @cacheControl(
maxAge: Int
inheritMaxAge: Boolean
scope: CacheControlScope
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION

""${'"'}
Configures cache settings for a field.

`@cacheControlField` is the same as `@cacheControl` but can be used on type system extensions for services that do not own the schema like
client services.

For example:

```graphql
# extend the schema to set a max age on User.email.
extend type User @cacheControlField(name: "email", maxAge: 20)
```
`User.email` is valid for 20 seconds.
""${'"'}
directive @cacheControlField(
name: String!
maxAge: Int
inheritMaxAge: Boolean
scope: CacheControlScope
) repeatable on OBJECT | INTERFACE
""".trimIndent()
2 changes: 2 additions & 0 deletions libraries/apollo-compiler/api/apollo-compiler.api
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ public final class com/apollographql/apollo/compiler/codegen/ResolverKeyKind : j
public static final field ArgumentDefinition Lcom/apollographql/apollo/compiler/codegen/ResolverKeyKind;
public static final field BuilderFun Lcom/apollographql/apollo/compiler/codegen/ResolverKeyKind;
public static final field BuilderType Lcom/apollographql/apollo/compiler/codegen/ResolverKeyKind;
public static final field Cache Lcom/apollographql/apollo/compiler/codegen/ResolverKeyKind;
public static final field CustomScalarAdapters Lcom/apollographql/apollo/compiler/codegen/ResolverKeyKind;
public static final field Fragment Lcom/apollographql/apollo/compiler/codegen/ResolverKeyKind;
public static final field FragmentSelections Lcom/apollographql/apollo/compiler/codegen/ResolverKeyKind;
Expand All @@ -576,6 +577,7 @@ public abstract interface class com/apollographql/apollo/compiler/codegen/Schema

public abstract interface class com/apollographql/apollo/compiler/codegen/SchemaLayout : com/apollographql/apollo/compiler/codegen/CommonLayout {
public abstract fun assertionsName ()Ljava/lang/String;
public abstract fun cacheName ()Ljava/lang/String;
public abstract fun paginationName ()Ljava/lang/String;
public abstract fun schemaName ()Ljava/lang/String;
public abstract fun schemaPackageName ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ internal class LayoutImpl(
return "Pagination"
}

override fun cacheName(): String {
return "Cache"
}

override fun operationName(name: String, capitalizedOperationType: String): String {
return className(name).let {
if (useSemanticNaming) {
Expand Down Expand Up @@ -156,6 +160,7 @@ internal fun SchemaLayout.typeAdapterPackageName() = "${schemaPackageName()}.typ
internal fun SchemaLayout.typeUtilPackageName() = "${schemaPackageName()}.type.util"

internal fun SchemaLayout.paginationPackageName() = "${schemaPackageName()}.pagination"
internal fun SchemaLayout.cachePackageName() = "${schemaPackageName()}.cache"
internal fun SchemaLayout.schemaSubPackageName() = "${schemaPackageName()}.schema"

internal fun SchemaLayout.javaOptionalAdapterClassName() = "OptionalAdapter"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ enum class ResolverKeyKind {
Schema,
CustomScalarAdapters,
Pagination,
Cache,
ArgumentDefinition,
JavaOptionalAdapter,
JavaOptionalAdapters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface SchemaLayout : CommonLayout {
fun schemaName(): String
fun assertionsName(): String
fun paginationName(): String
fun cacheName(): String
}

interface OperationsLayout: CommonLayout {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationBuil
import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationResponseAdapterBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationSelectionsBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.operations.OperationVariablesAdapterBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.CacheBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.CustomScalarAdaptersBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsEnumBuilder
import com.apollographql.apollo.compiler.codegen.kotlin.schema.EnumAsSealedBuilder
Expand Down Expand Up @@ -49,7 +50,6 @@ import com.apollographql.apollo.compiler.generateMethodsKotlin
import com.apollographql.apollo.compiler.ir.DefaultIrSchema
import com.apollographql.apollo.compiler.ir.IrOperations
import com.apollographql.apollo.compiler.ir.IrSchema
import com.apollographql.apollo.compiler.ir.IrTargetObject
import com.apollographql.apollo.compiler.maybeTransform
import com.apollographql.apollo.compiler.operationoutput.OperationOutput
import com.apollographql.apollo.compiler.operationoutput.findOperationId
Expand Down Expand Up @@ -206,6 +206,10 @@ internal object KotlinCodegen {
if (irSchema.connectionTypes.isNotEmpty() && context.resolver.resolve(ResolverKey(ResolverKeyKind.Pagination, "")) == null) {
builders.add(PaginationBuilder(context, irSchema.connectionTypes))
}

if (irSchema.maxAges.isNotEmpty() && context.resolver.resolve(ResolverKey(ResolverKeyKind.Cache, "")) == null) {
builders.add(CacheBuilder(context, irSchema.maxAges))
}
}
}

Expand Down
Loading
Loading