From e7f4df434e7ceb1c31bd3617ac2d269f683fe54f Mon Sep 17 00:00:00 2001 From: BoD Date: Wed, 13 Jul 2022 16:14:03 +0200 Subject: [PATCH 01/13] Add `connectionFields` argument to `@typePolicy` --- .../com/apollographql/apollo3/ast/Schema.kt | 8 +- .../ast/internal/SchemaValidationScope.kt | 85 +++- apollo-ast/src/main/resources/apollo.graphqls | 3 +- .../apollo3/compiler/codegen/CodegenLayout.kt | 4 + .../compiler/codegen/fieldPolicyArgs.kt | 31 +- .../compiler/codegen/kotlin/KotlinCodeGen.kt | 5 + .../compiler/codegen/kotlin/KotlinSymbols.kt | 1 + .../codegen/kotlin/file/PaginationBuilder.kt | 53 +++ .../apollo3/compiler/ir/IrBuilder.kt | 22 +- .../graphql/pagination/operations.graphql | 14 + .../graphql/pagination/schema.graphqls | 14 +- .../kotlin/TypePolicyConnectionFieldsTest.kt | 386 ++++++++++++++++++ 12 files changed, 607 insertions(+), 19 deletions(-) create mode 100644 apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/file/PaginationBuilder.kt create mode 100644 tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt index 0ddd206c60..c53cfa98a7 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt @@ -29,6 +29,7 @@ class Schema internal constructor( private val keyFields: Map>, val foreignNames: Map, private val directivesToStrip: List, + val connectionTypes: Set, ) { /** * Creates a new Schema from a list of definition. @@ -42,7 +43,8 @@ class Schema internal constructor( definitions, emptyMap(), emptyMap(), - emptyList() + emptyList(), + emptySet(), ) val typeDefinitions: Map = definitions @@ -137,7 +139,8 @@ class Schema internal constructor( "sdl" to GQLDocument(definitions, null).toUtf8(), "keyFields" to keyFields, "foreignNames" to foreignNames, - "directivesToStrip" to directivesToStrip + "directivesToStrip" to directivesToStrip, + "connectionTypes" to connectionTypes, ) } @@ -213,6 +216,7 @@ class Schema internal constructor( keyFields = (map["keyFields"]!! as Map>).mapValues { it.value.toSet() }, foreignNames = map["foreignNames"]!! as Map, directivesToStrip = map["directivesToStrip"]!! as List, + connectionTypes = (map["connectionTypes"]!! as List).toSet(), ) } } diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt index d77e0fd382..e06e553a21 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt @@ -1,9 +1,44 @@ package com.apollographql.apollo3.ast.internal import com.apollographql.apollo3.annotations.ApolloInternal -import com.apollographql.apollo3.ast.* +import com.apollographql.apollo3.ast.ConflictResolution +import com.apollographql.apollo3.ast.GQLDefinition +import com.apollographql.apollo3.ast.GQLDirective +import com.apollographql.apollo3.ast.GQLDirectiveDefinition +import com.apollographql.apollo3.ast.GQLEnumTypeDefinition +import com.apollographql.apollo3.ast.GQLField +import com.apollographql.apollo3.ast.GQLInputObjectTypeDefinition +import com.apollographql.apollo3.ast.GQLInterfaceTypeDefinition +import com.apollographql.apollo3.ast.GQLListType +import com.apollographql.apollo3.ast.GQLListValue +import com.apollographql.apollo3.ast.GQLNamed +import com.apollographql.apollo3.ast.GQLNamedType +import com.apollographql.apollo3.ast.GQLNonNullType +import com.apollographql.apollo3.ast.GQLObjectTypeDefinition +import com.apollographql.apollo3.ast.GQLObjectValue +import com.apollographql.apollo3.ast.GQLOperationTypeDefinition +import com.apollographql.apollo3.ast.GQLResult +import com.apollographql.apollo3.ast.GQLScalarTypeDefinition +import com.apollographql.apollo3.ast.GQLSchemaDefinition +import com.apollographql.apollo3.ast.GQLSchemaExtension +import com.apollographql.apollo3.ast.GQLStringValue +import com.apollographql.apollo3.ast.GQLType +import com.apollographql.apollo3.ast.GQLTypeDefinition import com.apollographql.apollo3.ast.GQLTypeDefinition.Companion.builtInTypes +import com.apollographql.apollo3.ast.GQLTypeSystemExtension +import com.apollographql.apollo3.ast.GQLUnionTypeDefinition +import com.apollographql.apollo3.ast.Issue +import com.apollographql.apollo3.ast.Schema import com.apollographql.apollo3.ast.Schema.Companion.TYPE_POLICY +import com.apollographql.apollo3.ast.SourceLocation +import com.apollographql.apollo3.ast.apolloDefinitions +import com.apollographql.apollo3.ast.builtinDefinitions +import com.apollographql.apollo3.ast.canHaveKeyFields +import com.apollographql.apollo3.ast.combineDefinitions +import com.apollographql.apollo3.ast.containsError +import com.apollographql.apollo3.ast.linkDefinitions +import com.apollographql.apollo3.ast.parseAsGQLSelections +import com.apollographql.apollo3.ast.transform2 internal fun validateSchema(definitions: List, requiresApolloDefinitions: Boolean = false): GQLResult { val issues = mutableListOf() @@ -133,6 +168,7 @@ internal fun validateSchema(definitions: List, requiresApolloDefi mergedScope.validateObjects() val keyFields = mergedScope.validateAndComputeKeyFields() + val connectionTypes = mergedScope.computeConnectionTypes() return if (issues.containsError()) { /** @@ -142,10 +178,11 @@ internal fun validateSchema(definitions: List, requiresApolloDefi } else { GQLResult( Schema( - mergedDefinitions, - keyFields, - foreignNames, - directivesToStrip + definitions = mergedDefinitions, + keyFields = keyFields, + foreignNames = foreignNames, + directivesToStrip = directivesToStrip, + connectionTypes = connectionTypes, ), issues ) @@ -452,6 +489,9 @@ private fun List.toKeyFields(): Set = extractFields("keyFi @ApolloInternal fun List.toEmbeddedFields(): Set = extractFields("embeddedFields") +@ApolloInternal +fun List.toConnectionFields(): Set = extractFields("connectionFields") + private fun List.extractFields(argumentName: String): Set { if (isEmpty()) { return emptySet() @@ -481,4 +521,37 @@ internal fun ValidationScope.validateAndComputeKeyFields(): Map { + val connectionTypes = mutableSetOf() + for (typeDefinition in typeDefinitions.values) { + val connectionFields = typeDefinition.directives.filter { originalDirectiveName(it.name) == TYPE_POLICY }.toConnectionFields() + for (fieldName in connectionFields) { + val field = typeDefinition.fields.firstOrNull { it.name == fieldName } ?: continue + connectionTypes.add(field.type.name) + } + } + return connectionTypes +} + +private val GQLTypeDefinition.directives + get() = when (this) { + is GQLObjectTypeDefinition -> directives + is GQLInterfaceTypeDefinition -> directives + else -> emptyList() + } + +private val GQLTypeDefinition.fields + get() = when (this) { + is GQLObjectTypeDefinition -> fields + is GQLInterfaceTypeDefinition -> fields + else -> emptyList() + } + +private val GQLType.name: String + get() = when (this) { + is GQLNonNullType -> type.name + is GQLListType -> type.name + is GQLNamedType -> name + } diff --git a/apollo-ast/src/main/resources/apollo.graphqls b/apollo-ast/src/main/resources/apollo.graphqls index 2f856d4a8b..fd19ba7f61 100644 --- a/apollo-ast/src/main/resources/apollo.graphqls +++ b/apollo-ast/src/main/resources/apollo.graphqls @@ -22,7 +22,8 @@ directive @nonnull(fields: String! = "") on OBJECT | FIELD # `keyFields`: a selection set containing fields used to compute the cache key of an object. Order is important. # `embeddedFields`: a selection set containing fields that shouldn't create a new cache Record and should be # embedded in their parent instead. Order is unimportant. -directive @typePolicy(keyFields: String! = "", embeddedFields: String! = "") on OBJECT | INTERFACE | UNION +# `connectionFields`: a selection set containing fields that should be treated as Relay Connection fields. Order is unimportant. +directive @typePolicy(keyFields: String! = "", embeddedFields: String! = "", connectionFields: String! = "") on OBJECT | INTERFACE | UNION # Attach extra information to a given field # `keyArgs`: a list of arguments used to compute the cache key of the object this field is pointing to. diff --git a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/CodegenLayout.kt b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/CodegenLayout.kt index fc2b65e719..420d026bd7 100644 --- a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/CodegenLayout.kt +++ b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/CodegenLayout.kt @@ -81,6 +81,8 @@ internal abstract class CodegenLayout( fun fragmentAdapterPackageName(filePath: String) = "${fragmentPackageName(filePath)}.adapter".stripDots() fun fragmentResponseFieldsPackageName(filePath: String) = "${fragmentPackageName(filePath)}.selections".stripDots() + fun paginationPackageName() = "$schemaPackageName.pagination" + private fun String.stripDots() = this.removePrefix(".").removeSuffix(".") // ------------------------ Names --------------------------------- @@ -110,6 +112,8 @@ internal abstract class CodegenLayout( internal fun operationVariablesAdapterName(operation: IrOperation) = operationName(operation) + "_VariablesAdapter" internal fun operationSelectionsName(operation: IrOperation) = operationName(operation) + "Selections" + internal fun paginationName() = "Pagination" + internal fun fragmentName(name: String) = capitalizedIdentifier(name) + "Impl" internal fun fragmentResponseAdapterWrapperName(name: String) = fragmentName(name) + "_ResponseAdapter" internal fun fragmentVariablesAdapterName(name: String) = fragmentName(name) + "_VariablesAdapter" diff --git a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/fieldPolicyArgs.kt b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/fieldPolicyArgs.kt index 256a19c86e..2fc7b2fea5 100644 --- a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/fieldPolicyArgs.kt +++ b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/fieldPolicyArgs.kt @@ -7,6 +7,7 @@ import com.apollographql.apollo3.ast.GQLStringValue import com.apollographql.apollo3.ast.GQLTypeDefinition import com.apollographql.apollo3.ast.Schema import com.apollographql.apollo3.ast.SourceAwareException +import com.apollographql.apollo3.ast.internal.toConnectionFields import com.apollographql.apollo3.ast.parseAsGQLSelections import okio.Buffer @@ -18,15 +19,10 @@ internal fun GQLTypeDefinition.keyArgs( internal fun GQLTypeDefinition.paginationArgs( fieldName: String, schema: Schema, -): Set = fieldPolicyArgs(Schema.FIELD_POLICY_PAGINATION_ARGS, fieldName, schema) +): Set = fieldPolicyArgs(Schema.FIELD_POLICY_PAGINATION_ARGS, fieldName, schema) + + typePolicyConnectionArgs(fieldName, schema) private fun GQLTypeDefinition.fieldPolicyArgs(argumentName: String, fieldName: String, schema: Schema): Set { - val directives = when (this) { - is GQLObjectTypeDefinition -> directives - is GQLInterfaceTypeDefinition -> directives - else -> emptyList() - } - return directives.filter { schema.originalDirectiveName(it.name) == Schema.FIELD_POLICY }.filter { (it.arguments?.arguments?.single { it.name == Schema.FIELD_POLICY_FOR_FIELD }?.value as GQLStringValue).value == fieldName }.flatMap { @@ -50,3 +46,24 @@ private fun GQLTypeDefinition.fieldPolicyArgs(argumentName: String, fieldName: S } ?: throw SourceAwareException("Apollo: $argumentName should be a selectionSet", it.sourceLocation) }.toSet() } + +/** + * If [fieldName] is in the `connectionFields` argument of a `@typePolicy` directive of its parent type, return + * the standard Relay Connection arguments to be ignored for pagination. + * Otherwise, return an empty set. + */ +private fun GQLTypeDefinition.typePolicyConnectionArgs(fieldName: String, schema: Schema): Set { + val connectionFields = directives.filter { schema.originalDirectiveName(it.name) == Schema.TYPE_POLICY }.toConnectionFields() + return if (fieldName !in connectionFields) { + emptySet() + } else { + setOf("before", "after", "first", "last") + } +} + +private val GQLTypeDefinition.directives + get() = when (this) { + is GQLObjectTypeDefinition -> directives + is GQLInterfaceTypeDefinition -> directives + else -> emptyList() + } diff --git a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinCodeGen.kt b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinCodeGen.kt index 1f824741cc..11156d559b 100644 --- a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinCodeGen.kt +++ b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinCodeGen.kt @@ -22,6 +22,7 @@ import com.apollographql.apollo3.compiler.codegen.kotlin.file.OperationBuilder import com.apollographql.apollo3.compiler.codegen.kotlin.file.OperationResponseAdapterBuilder import com.apollographql.apollo3.compiler.codegen.kotlin.file.OperationSelectionsBuilder import com.apollographql.apollo3.compiler.codegen.kotlin.file.OperationVariablesAdapterBuilder +import com.apollographql.apollo3.compiler.codegen.kotlin.file.PaginationBuilder import com.apollographql.apollo3.compiler.codegen.kotlin.file.SchemaBuilder import com.apollographql.apollo3.compiler.codegen.kotlin.file.TestBuildersBuilder import com.apollographql.apollo3.compiler.codegen.kotlin.file.UnionBuilder @@ -197,6 +198,10 @@ internal class KotlinCodeGen( builders.add(SchemaBuilder(context, generatedSchemaName, ir.objects, ir.interfaces, ir.unions)) } + if (ir.schema.connectionTypes.isNotEmpty()) { + builders.add(PaginationBuilder(context, ir.schema.connectionTypes)) + } + /** * 1st pass: call prepare on all builders */ diff --git a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinSymbols.kt b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinSymbols.kt index f7318bc0c8..e5785e9ea4 100644 --- a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinSymbols.kt +++ b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/KotlinSymbols.kt @@ -71,6 +71,7 @@ internal object KotlinSymbols { val List = ClassName("kotlin.collections", "List") val Map = ClassName("kotlin.collections", "Map") val Array = ClassName("kotlin", "Array") + val Set = ClassName("kotlin.collections", "Set") val Suppress = ClassName("kotlin", "Suppress") val JvmOverloads = ClassName("kotlin.jvm", "JvmOverloads") diff --git a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/file/PaginationBuilder.kt b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/file/PaginationBuilder.kt new file mode 100644 index 0000000000..97dffa30ff --- /dev/null +++ b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/codegen/kotlin/file/PaginationBuilder.kt @@ -0,0 +1,53 @@ +package com.apollographql.apollo3.compiler.codegen.kotlin.file + +import com.apollographql.apollo3.compiler.codegen.kotlin.CgFile +import com.apollographql.apollo3.compiler.codegen.kotlin.CgFileBuilder +import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinContext +import com.apollographql.apollo3.compiler.codegen.kotlin.KotlinSymbols +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec + +internal class PaginationBuilder( + context: KotlinContext, + private val connectionTypes: Set, +) : CgFileBuilder { + private val layout = context.layout + private val packageName = layout.paginationPackageName() + private val simpleName = layout.paginationName() + + override fun prepare() { + } + + override fun build(): CgFile { + return CgFile( + packageName = packageName, + fileName = simpleName, + typeSpecs = listOf(typeSpec()) + ) + } + + private fun typeSpec(): TypeSpec { + return TypeSpec.objectBuilder(simpleName) + .addProperty(connectionTypesPropertySpec()) + .build() + } + + private fun connectionTypesPropertySpec(): PropertySpec { + val builder = CodeBlock.builder() + builder.add("setOf(\n") + builder.indent() + builder.add( + connectionTypes.map { + CodeBlock.of("%S", it) + }.joinToString(", ") + ) + builder.unindent() + builder.add(")\n") + + return PropertySpec.builder("connectionTypes", KotlinSymbols.Set.parameterizedBy(KotlinSymbols.String)) + .initializer(builder.build()) + .build() + } +} diff --git a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ir/IrBuilder.kt b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ir/IrBuilder.kt index a3c884e6e9..42632b345f 100644 --- a/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ir/IrBuilder.kt +++ b/apollo-compiler/src/main/kotlin/com/apollographql/apollo3/compiler/ir/IrBuilder.kt @@ -47,6 +47,7 @@ import com.apollographql.apollo3.ast.findNonnull import com.apollographql.apollo3.ast.findOptInFeature import com.apollographql.apollo3.ast.findTargetName import com.apollographql.apollo3.ast.inferVariables +import com.apollographql.apollo3.ast.internal.toConnectionFields import com.apollographql.apollo3.ast.internal.toEmbeddedFields import com.apollographql.apollo3.ast.isFieldNonNull import com.apollographql.apollo3.ast.leafType @@ -188,7 +189,9 @@ internal class IrBuilder( description = description, // XXX: this is not spec-compliant. Directive cannot be on object definitions deprecationReason = directives.findDeprecationReason(), - embeddedFields = directives.filter { schema.originalDirectiveName(it.name)== TYPE_POLICY }.toEmbeddedFields() + embeddedFields = directives.filter { schema.originalDirectiveName(it.name) == TYPE_POLICY }.toEmbeddedFields() + + directives.filter { schema.originalDirectiveName(it.name) == TYPE_POLICY }.toConnectionFields() + + connectionTypeEmbeddedFields(name, schema), ) } @@ -204,10 +207,25 @@ internal class IrBuilder( description = description, // XXX: this is not spec-compliant. Directive cannot be on interfaces deprecationReason = directives.findDeprecationReason(), - embeddedFields = directives.filter { schema.originalDirectiveName(it.name)== TYPE_POLICY }.toEmbeddedFields() + embeddedFields = directives.filter { schema.originalDirectiveName(it.name) == TYPE_POLICY }.toEmbeddedFields() + + directives.filter { schema.originalDirectiveName(it.name) == TYPE_POLICY }.toConnectionFields() + + connectionTypeEmbeddedFields(name, schema), ) } + /** + * If [typeName] is declared as a Relay Connection type (via the `@typePolicy` directive), return the standard arguments + * to be embedded. + * Otherwise, return an empty set. + */ + private fun connectionTypeEmbeddedFields(typeName: String, schema: Schema): Set { + return if (typeName in schema.connectionTypes) { + setOf("edges") + } else { + emptySet() + } + } + private fun GQLUnionTypeDefinition.toIr(): IrUnion { // Needed to build the compiled type usedTypes.addAll(memberTypes.map { it.name }) diff --git a/tests/pagination/src/commonMain/graphql/pagination/operations.graphql b/tests/pagination/src/commonMain/graphql/pagination/operations.graphql index 732500977d..81ee70db05 100644 --- a/tests/pagination/src/commonMain/graphql/pagination/operations.graphql +++ b/tests/pagination/src/commonMain/graphql/pagination/operations.graphql @@ -11,6 +11,20 @@ query UsersCursorBased($first: Int, $after: String, $last: Int, $before: String) } } +query WithTypePolicyDirective($first: Int, $after: String, $last: Int, $before: String) { + usersCursorBased2(first: $first, after: $after, last: $last, before: $before) { + edges { + cursor + node { + id + name + email + } + } + } +} + + query UsersOffsetBasedWithArray($offset: Int, $limit: Int) { usersOffsetBasedWithArray(offset: $offset, limit: $limit) { id diff --git a/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls b/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls index 04254861d7..13159f9835 100644 --- a/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls +++ b/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls @@ -1,10 +1,12 @@ type Query -@typePolicy(embeddedFields: "usersCursorBased, usersOffsetBasedWithPage") +@typePolicy(embeddedFields: "usersCursorBased, usersOffsetBasedWithPage" connectionFields: "usersCursorBased2, usersCursorBased3") @fieldPolicy(forField: "usersCursorBased", paginationArgs: "first, after, last, before") @fieldPolicy(forField: "usersOffsetBasedWithArray", paginationArgs: "offset, limit") @fieldPolicy(forField: "usersOffsetBasedWithPage", paginationArgs: "offset, limit") { usersCursorBased(first: Int = 10, after: String = null, last: Int = null, before: String = null): UserConnection! + usersCursorBased2(first: Int = 10, after: String = null, last: Int = null, before: String = null): UserConnection2! + usersCursorBased3(first: Int = 10, after: String = null, last: Int = null, before: String = null): UserConnection3! usersOffsetBasedWithArray(offset: Int = 0, limit: Int = 10): [User!]! @@ -16,6 +18,16 @@ type UserConnection @typePolicy(embeddedFields: "pageInfo, edges") { edges: [UserEdge!]! } +type UserConnection2 { + pageInfo: PageInfo! + edges: [UserEdge!]! +} + +type UserConnection3 { + pageInfo: PageInfo! + edges: [UserEdge!]! +} + type PageInfo { startCursor: String! endCursor: String! diff --git a/tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt b/tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt new file mode 100644 index 0000000000..f44b9c0780 --- /dev/null +++ b/tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt @@ -0,0 +1,386 @@ +package pagination + +import com.apollographql.apollo3.api.Optional +import com.apollographql.apollo3.cache.normalized.ApolloStore +import com.apollographql.apollo3.cache.normalized.api.FieldPolicyApolloResolver +import com.apollographql.apollo3.cache.normalized.api.FieldRecordMerger +import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory +import com.apollographql.apollo3.cache.normalized.api.MetadataGenerator +import com.apollographql.apollo3.cache.normalized.api.MetadataGeneratorContext +import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory +import com.apollographql.apollo3.cache.normalized.api.TypePolicyCacheKeyGenerator +import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory +import com.apollographql.apollo3.testing.runTest +import pagination.pagination.Pagination +import pagination.test.WithTypePolicyDirectiveQuery_TestBuilder.Data +import kotlin.test.Test +import kotlin.test.assertEquals + +class TypePolicyConnectionFieldsTest { + @Test + fun typePolicyConnectionFieldsMemoryCache() { + typePolicyConnectionFields(MemoryCacheFactory()) + } + + @Test + fun typePolicyConnectionFieldsBlobSqlCache() { + typePolicyConnectionFields(SqlNormalizedCacheFactory(name = "blob", withDates = true)) + } + + @Test + fun typePolicyConnectionFieldsJsonSqlCache() { + typePolicyConnectionFields(SqlNormalizedCacheFactory(name = "json", withDates = false)) + } + + @Test + fun typePolicyConnectionFieldsChainedCache() { + typePolicyConnectionFields(MemoryCacheFactory().chain(SqlNormalizedCacheFactory(name = "json", withDates = false))) + } + + private fun typePolicyConnectionFields(cacheFactory: NormalizedCacheFactory) = runTest { + val apolloStore = ApolloStore( + normalizedCacheFactory = cacheFactory, + cacheKeyGenerator = TypePolicyCacheKeyGenerator, + metadataGenerator = CursorPaginationMetadataGenerator(Pagination.connectionTypes), + apolloResolver = FieldPolicyApolloResolver, + recordMerger = FieldRecordMerger(CursorPaginationFieldMerger()) + ) + apolloStore.clearAll() + + // First page + val query1 = WithTypePolicyDirectiveQuery(first = Optional.Present(2)) + val data1 = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx42" + node = node { + id = "42" + } + }, + edge { + cursor = "xx43" + node = node { + id = "43" + } + }, + ) + } + } + apolloStore.writeOperation(query1, data1) + var dataFromStore = apolloStore.readOperation(query1) + assertEquals(data1, dataFromStore) + assertChainedCachesAreEqual(apolloStore) + + // Page after + val query2 = WithTypePolicyDirectiveQuery(first = Optional.Present(2), after = Optional.Present("xx43")) + val data2 = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx44" + node = node { + id = "44" + } + }, + edge { + cursor = "xx45" + node = node { + id = "45" + } + }, + ) + } + } + apolloStore.writeOperation(query2, data2) + dataFromStore = apolloStore.readOperation(query1) + var expectedData = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx42" + node = node { + id = "42" + } + }, + edge { + cursor = "xx43" + node = node { + id = "43" + } + }, + edge { + cursor = "xx44" + node = node { + id = "44" + } + }, + edge { + cursor = "xx45" + node = node { + id = "45" + } + }, + ) + } + } + assertEquals(expectedData, dataFromStore) + assertChainedCachesAreEqual(apolloStore) + + // Page after + val query3 = WithTypePolicyDirectiveQuery(first = Optional.Present(2), after = Optional.Present("xx45")) + val data3 = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx46" + node = node { + id = "46" + } + }, + edge { + cursor = "xx47" + node = node { + id = "47" + } + }, + ) + } + } + apolloStore.writeOperation(query3, data3) + dataFromStore = apolloStore.readOperation(query1) + expectedData = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx42" + node = node { + id = "42" + } + }, + edge { + cursor = "xx43" + node = node { + id = "43" + } + }, + edge { + cursor = "xx44" + node = node { + id = "44" + } + }, + edge { + cursor = "xx45" + node = node { + id = "45" + } + }, + edge { + cursor = "xx46" + node = node { + id = "46" + } + }, + edge { + cursor = "xx47" + node = node { + id = "47" + } + }, + ) + } + } + assertEquals(expectedData, dataFromStore) + assertChainedCachesAreEqual(apolloStore) + + // Page before + val query4 = WithTypePolicyDirectiveQuery(last = Optional.Present(2), before = Optional.Present("xx42")) + val data4 = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx40" + node = node { + id = "40" + } + }, + edge { + cursor = "xx41" + node = node { + id = "41" + } + }, + ) + } + } + apolloStore.writeOperation(query4, data4) + dataFromStore = apolloStore.readOperation(query1) + expectedData = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx40" + node = node { + id = "40" + } + }, + edge { + cursor = "xx41" + node = node { + id = "41" + } + }, + edge { + cursor = "xx42" + node = node { + id = "42" + } + }, + edge { + cursor = "xx43" + node = node { + id = "43" + } + }, + edge { + cursor = "xx44" + node = node { + id = "44" + } + }, + edge { + cursor = "xx45" + node = node { + id = "45" + } + }, + edge { + cursor = "xx46" + node = node { + id = "46" + } + }, + edge { + cursor = "xx47" + node = node { + id = "47" + } + }, + ) + } + } + assertEquals(expectedData, dataFromStore) + assertChainedCachesAreEqual(apolloStore) + + // Non-contiguous page (should reset) + val query5 = WithTypePolicyDirectiveQuery(first = Optional.Present(2), after = Optional.Present("xx50")) + val data5 = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = listOf( + edge { + cursor = "xx50" + node = node { + id = "50" + } + }, + edge { + cursor = "xx51" + node = node { + id = "51" + } + }, + ) + } + } + apolloStore.writeOperation(query5, data5) + dataFromStore = apolloStore.readOperation(query1) + assertEquals(data5, dataFromStore) + assertChainedCachesAreEqual(apolloStore) + + // Empty page (should keep previous result) + val query6 = WithTypePolicyDirectiveQuery(first = Optional.Present(2), after = Optional.Present("xx51")) + val data6 = WithTypePolicyDirectiveQuery.Data { + usersCursorBased2 = usersCursorBased2 { + edges = emptyList() + } + } + apolloStore.writeOperation(query6, data6) + dataFromStore = apolloStore.readOperation(query1) + assertEquals(data5, dataFromStore) + assertChainedCachesAreEqual(apolloStore) + } + + @Suppress("UNCHECKED_CAST") + private class CursorPaginationMetadataGenerator(private val connectionTypes: Set) : MetadataGenerator { + override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map { + if (context.field.type.leafType().name in connectionTypes) { + obj as Map + val edges = obj["edges"] as List> + val startCursor = edges.firstOrNull()?.get("cursor") as String? + val endCursor = edges.lastOrNull()?.get("cursor") as String? + return mapOf( + "startCursor" to startCursor, + "endCursor" to endCursor, + "before" to context.argumentValue("before"), + "after" to context.argumentValue("after"), + ) + } + return emptyMap() + } + } + + private class CursorPaginationFieldMerger : FieldRecordMerger.FieldMerger { + @Suppress("UNCHECKED_CAST") + override fun mergeFields(existing: FieldRecordMerger.FieldInfo, incoming: FieldRecordMerger.FieldInfo): FieldRecordMerger.FieldInfo { + val existingStartCursor = existing.metadata["startCursor"] as? String + val existingEndCursor = existing.metadata["endCursor"] as? String + val incomingStartCursor = incoming.metadata["startCursor"] as? String + val incomingEndCursor = incoming.metadata["endCursor"] as? String + val incomingBeforeArgument = incoming.metadata["before"] as? String + val incomingAfterArgument = incoming.metadata["after"] as? String + + return if (incomingBeforeArgument == null && incomingAfterArgument == null) { + // Not a pagination query + incoming + } else if (existingStartCursor == null || existingEndCursor == null) { + // Existing is empty + incoming + } else if (incomingStartCursor == null || incomingEndCursor == null) { + // Incoming is empty + existing + } else { + val existingValue = existing.value as Map + val existingList = existingValue["edges"] as List<*> + val incomingList = (incoming.value as Map)["edges"] as List<*> + + val mergedList: List<*> + val newStartCursor: String + val newEndCursor: String + if (incomingAfterArgument == existingEndCursor) { + mergedList = existingList + incomingList + newStartCursor = existingStartCursor + newEndCursor = incomingEndCursor + } else if (incomingBeforeArgument == existingStartCursor) { + mergedList = incomingList + existingList + newStartCursor = incomingStartCursor + newEndCursor = existingEndCursor + } else { + // We received a list which is neither the previous nor the next page. + // Handle this case by resetting the cache with this page + mergedList = incomingList + newStartCursor = incomingStartCursor + newEndCursor = incomingEndCursor + } + + val mergedFieldValue = existingValue.toMutableMap() + mergedFieldValue["edges"] = mergedList + FieldRecordMerger.FieldInfo( + value = mergedFieldValue, + metadata = mapOf("startCursor" to newStartCursor, "endCursor" to newEndCursor) + ) + } + } + } +} + From afe608fb9652ebee893f2193893bc06fd90a7616 Mon Sep 17 00:00:00 2001 From: BoD Date: Wed, 13 Jul 2022 17:04:08 +0200 Subject: [PATCH 02/13] Add Relay/Connection specific MetadataGenerator and RecordMerger --- .../cache/normalized/api/MetadataGenerator.kt | 20 +++++ .../cache/normalized/api/RecordMerger.kt | 56 +++++++++++++ .../kotlin/CursorBasedPaginationTest.kt | 81 +------------------ .../kotlin/TypePolicyConnectionFieldsTest.kt | 81 +------------------ 4 files changed, 84 insertions(+), 154 deletions(-) diff --git a/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/MetadataGenerator.kt b/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/MetadataGenerator.kt index d966b4eb93..daaf0a7c06 100644 --- a/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/MetadataGenerator.kt +++ b/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/MetadataGenerator.kt @@ -27,3 +27,23 @@ class MetadataGeneratorContext( object EmptyMetadataGenerator : MetadataGenerator { override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map = emptyMap() } + +@ApolloExperimental +class ConnectionMetadataGenerator(private val connectionTypes: Set) : MetadataGenerator { + @Suppress("UNCHECKED_CAST") + override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map { + if (context.field.type.leafType().name in connectionTypes) { + obj as Map + val edges = obj["edges"] as List> + val startCursor = edges.firstOrNull()?.get("cursor") as String? + val endCursor = edges.lastOrNull()?.get("cursor") as String? + return mapOf( + "startCursor" to startCursor, + "endCursor" to endCursor, + "before" to context.argumentValue("before"), + "after" to context.argumentValue("after"), + ) + } + return emptyMap() + } +} diff --git a/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/RecordMerger.kt b/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/RecordMerger.kt index 64e729d942..7507b136fe 100644 --- a/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/RecordMerger.kt +++ b/apollo-normalized-cache-api-incubating/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/RecordMerger.kt @@ -94,3 +94,59 @@ class FieldRecordMerger(private val fieldMerger: FieldMerger) : RecordMerger { ) to changedKeys } } + +@ApolloExperimental +val ConnectionRecordMerger = FieldRecordMerger(ConnectionFieldMerger) + +private object ConnectionFieldMerger : FieldRecordMerger.FieldMerger { + @Suppress("UNCHECKED_CAST") + override fun mergeFields(existing: FieldRecordMerger.FieldInfo, incoming: FieldRecordMerger.FieldInfo): FieldRecordMerger.FieldInfo { + val existingStartCursor = existing.metadata["startCursor"] as? String + val existingEndCursor = existing.metadata["endCursor"] as? String + val incomingStartCursor = incoming.metadata["startCursor"] as? String + val incomingEndCursor = incoming.metadata["endCursor"] as? String + val incomingBeforeArgument = incoming.metadata["before"] as? String + val incomingAfterArgument = incoming.metadata["after"] as? String + + return if (incomingBeforeArgument == null && incomingAfterArgument == null) { + // Not a pagination query + incoming + } else if (existingStartCursor == null || existingEndCursor == null) { + // Existing is empty + incoming + } else if (incomingStartCursor == null || incomingEndCursor == null) { + // Incoming is empty + existing + } else { + val existingValue = existing.value as Map + val existingList = existingValue["edges"] as List<*> + val incomingList = (incoming.value as Map)["edges"] as List<*> + + val mergedList: List<*> + val newStartCursor: String + val newEndCursor: String + if (incomingAfterArgument == existingEndCursor) { + mergedList = existingList + incomingList + newStartCursor = existingStartCursor + newEndCursor = incomingEndCursor + } else if (incomingBeforeArgument == existingStartCursor) { + mergedList = incomingList + existingList + newStartCursor = incomingStartCursor + newEndCursor = existingEndCursor + } else { + // We received a list which is neither the previous nor the next page. + // Handle this case by resetting the cache with this page + mergedList = incomingList + newStartCursor = incomingStartCursor + newEndCursor = incomingEndCursor + } + + val mergedFieldValue = existingValue.toMutableMap() + mergedFieldValue["edges"] = mergedList + FieldRecordMerger.FieldInfo( + value = mergedFieldValue, + metadata = mapOf("startCursor" to newStartCursor, "endCursor" to newEndCursor) + ) + } + } +} diff --git a/tests/pagination/src/commonTest/kotlin/CursorBasedPaginationTest.kt b/tests/pagination/src/commonTest/kotlin/CursorBasedPaginationTest.kt index 3e9622f004..293c1327cd 100644 --- a/tests/pagination/src/commonTest/kotlin/CursorBasedPaginationTest.kt +++ b/tests/pagination/src/commonTest/kotlin/CursorBasedPaginationTest.kt @@ -2,11 +2,10 @@ package pagination import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.cache.normalized.ApolloStore +import com.apollographql.apollo3.cache.normalized.api.ConnectionMetadataGenerator +import com.apollographql.apollo3.cache.normalized.api.ConnectionRecordMerger import com.apollographql.apollo3.cache.normalized.api.FieldPolicyApolloResolver -import com.apollographql.apollo3.cache.normalized.api.FieldRecordMerger import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory -import com.apollographql.apollo3.cache.normalized.api.MetadataGenerator -import com.apollographql.apollo3.cache.normalized.api.MetadataGeneratorContext import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory import com.apollographql.apollo3.cache.normalized.api.TypePolicyCacheKeyGenerator import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory @@ -40,9 +39,9 @@ class CursorBasedPaginationTest { val apolloStore = ApolloStore( normalizedCacheFactory = cacheFactory, cacheKeyGenerator = TypePolicyCacheKeyGenerator, - metadataGenerator = CursorPaginationMetadataGenerator(setOf("UserConnection")), + metadataGenerator = ConnectionMetadataGenerator(setOf("UserConnection")), apolloResolver = FieldPolicyApolloResolver, - recordMerger = FieldRecordMerger(CursorPaginationFieldMerger()) + recordMerger = ConnectionRecordMerger ) apolloStore.clearAll() @@ -309,77 +308,5 @@ class CursorBasedPaginationTest { assertEquals(data5, dataFromStore) assertChainedCachesAreEqual(apolloStore) } - - @Suppress("UNCHECKED_CAST") - private class CursorPaginationMetadataGenerator(private val connectionTypes: Set) : MetadataGenerator { - override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map { - if (context.field.type.leafType().name in connectionTypes) { - obj as Map - val edges = obj["edges"] as List> - val startCursor = edges.firstOrNull()?.get("cursor") as String? - val endCursor = edges.lastOrNull()?.get("cursor") as String? - return mapOf( - "startCursor" to startCursor, - "endCursor" to endCursor, - "before" to context.argumentValue("before"), - "after" to context.argumentValue("after"), - ) - } - return emptyMap() - } - } - - private class CursorPaginationFieldMerger : FieldRecordMerger.FieldMerger { - @Suppress("UNCHECKED_CAST") - override fun mergeFields(existing: FieldRecordMerger.FieldInfo, incoming: FieldRecordMerger.FieldInfo): FieldRecordMerger.FieldInfo { - val existingStartCursor = existing.metadata["startCursor"] as? String - val existingEndCursor = existing.metadata["endCursor"] as? String - val incomingStartCursor = incoming.metadata["startCursor"] as? String - val incomingEndCursor = incoming.metadata["endCursor"] as? String - val incomingBeforeArgument = incoming.metadata["before"] as? String - val incomingAfterArgument = incoming.metadata["after"] as? String - - return if (incomingBeforeArgument == null && incomingAfterArgument == null) { - // Not a pagination query - incoming - } else if (existingStartCursor == null || existingEndCursor == null) { - // Existing is empty - incoming - } else if (incomingStartCursor == null || incomingEndCursor == null) { - // Incoming is empty - existing - } else { - val existingValue = existing.value as Map - val existingList = existingValue["edges"] as List<*> - val incomingList = (incoming.value as Map)["edges"] as List<*> - - val mergedList: List<*> - val newStartCursor: String - val newEndCursor: String - if (incomingAfterArgument == existingEndCursor) { - mergedList = existingList + incomingList - newStartCursor = existingStartCursor - newEndCursor = incomingEndCursor - } else if (incomingBeforeArgument == existingStartCursor) { - mergedList = incomingList + existingList - newStartCursor = incomingStartCursor - newEndCursor = existingEndCursor - } else { - // We received a list which is neither the previous nor the next page. - // Handle this case by resetting the cache with this page - mergedList = incomingList - newStartCursor = incomingStartCursor - newEndCursor = incomingEndCursor - } - - val mergedFieldValue = existingValue.toMutableMap() - mergedFieldValue["edges"] = mergedList - FieldRecordMerger.FieldInfo( - value = mergedFieldValue, - metadata = mapOf("startCursor" to newStartCursor, "endCursor" to newEndCursor) - ) - } - } - } } diff --git a/tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt b/tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt index f44b9c0780..7272c7797c 100644 --- a/tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt +++ b/tests/pagination/src/commonTest/kotlin/TypePolicyConnectionFieldsTest.kt @@ -2,11 +2,10 @@ package pagination import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.cache.normalized.ApolloStore +import com.apollographql.apollo3.cache.normalized.api.ConnectionMetadataGenerator +import com.apollographql.apollo3.cache.normalized.api.ConnectionRecordMerger import com.apollographql.apollo3.cache.normalized.api.FieldPolicyApolloResolver -import com.apollographql.apollo3.cache.normalized.api.FieldRecordMerger import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory -import com.apollographql.apollo3.cache.normalized.api.MetadataGenerator -import com.apollographql.apollo3.cache.normalized.api.MetadataGeneratorContext import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory import com.apollographql.apollo3.cache.normalized.api.TypePolicyCacheKeyGenerator import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory @@ -41,9 +40,9 @@ class TypePolicyConnectionFieldsTest { val apolloStore = ApolloStore( normalizedCacheFactory = cacheFactory, cacheKeyGenerator = TypePolicyCacheKeyGenerator, - metadataGenerator = CursorPaginationMetadataGenerator(Pagination.connectionTypes), + metadataGenerator = ConnectionMetadataGenerator(Pagination.connectionTypes), apolloResolver = FieldPolicyApolloResolver, - recordMerger = FieldRecordMerger(CursorPaginationFieldMerger()) + recordMerger = ConnectionRecordMerger ) apolloStore.clearAll() @@ -310,77 +309,5 @@ class TypePolicyConnectionFieldsTest { assertEquals(data5, dataFromStore) assertChainedCachesAreEqual(apolloStore) } - - @Suppress("UNCHECKED_CAST") - private class CursorPaginationMetadataGenerator(private val connectionTypes: Set) : MetadataGenerator { - override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map { - if (context.field.type.leafType().name in connectionTypes) { - obj as Map - val edges = obj["edges"] as List> - val startCursor = edges.firstOrNull()?.get("cursor") as String? - val endCursor = edges.lastOrNull()?.get("cursor") as String? - return mapOf( - "startCursor" to startCursor, - "endCursor" to endCursor, - "before" to context.argumentValue("before"), - "after" to context.argumentValue("after"), - ) - } - return emptyMap() - } - } - - private class CursorPaginationFieldMerger : FieldRecordMerger.FieldMerger { - @Suppress("UNCHECKED_CAST") - override fun mergeFields(existing: FieldRecordMerger.FieldInfo, incoming: FieldRecordMerger.FieldInfo): FieldRecordMerger.FieldInfo { - val existingStartCursor = existing.metadata["startCursor"] as? String - val existingEndCursor = existing.metadata["endCursor"] as? String - val incomingStartCursor = incoming.metadata["startCursor"] as? String - val incomingEndCursor = incoming.metadata["endCursor"] as? String - val incomingBeforeArgument = incoming.metadata["before"] as? String - val incomingAfterArgument = incoming.metadata["after"] as? String - - return if (incomingBeforeArgument == null && incomingAfterArgument == null) { - // Not a pagination query - incoming - } else if (existingStartCursor == null || existingEndCursor == null) { - // Existing is empty - incoming - } else if (incomingStartCursor == null || incomingEndCursor == null) { - // Incoming is empty - existing - } else { - val existingValue = existing.value as Map - val existingList = existingValue["edges"] as List<*> - val incomingList = (incoming.value as Map)["edges"] as List<*> - - val mergedList: List<*> - val newStartCursor: String - val newEndCursor: String - if (incomingAfterArgument == existingEndCursor) { - mergedList = existingList + incomingList - newStartCursor = existingStartCursor - newEndCursor = incomingEndCursor - } else if (incomingBeforeArgument == existingStartCursor) { - mergedList = incomingList + existingList - newStartCursor = incomingStartCursor - newEndCursor = existingEndCursor - } else { - // We received a list which is neither the previous nor the next page. - // Handle this case by resetting the cache with this page - mergedList = incomingList - newStartCursor = incomingStartCursor - newEndCursor = incomingEndCursor - } - - val mergedFieldValue = existingValue.toMutableMap() - mergedFieldValue["edges"] = mergedList - FieldRecordMerger.FieldInfo( - value = mergedFieldValue, - metadata = mapOf("startCursor" to newStartCursor, "endCursor" to newEndCursor) - ) - } - } - } } From 719f05cbd8f6e94a16497b34d8dab032d60c8914 Mon Sep 17 00:00:00 2001 From: BoD Date: Wed, 13 Jul 2022 17:06:25 +0200 Subject: [PATCH 03/13] Update API dump --- apollo-ast/api/apollo-ast.api | 1 + 1 file changed, 1 insertion(+) diff --git a/apollo-ast/api/apollo-ast.api b/apollo-ast/api/apollo-ast.api index 98dc4045dc..90be08266d 100644 --- a/apollo-ast/api/apollo-ast.api +++ b/apollo-ast/api/apollo-ast.api @@ -1121,6 +1121,7 @@ public final class com/apollographql/apollo3/ast/Schema { public static final field REQUIRES_OPT_IN Ljava/lang/String; public static final field TYPE_POLICY Ljava/lang/String; public fun (Ljava/util/List;)V + public final fun getConnectionTypes ()Ljava/util/Set; public final fun getDirectiveDefinitions ()Ljava/util/Map; public final fun getForeignNames ()Ljava/util/Map; public final fun getMutationTypeDefinition ()Lcom/apollographql/apollo3/ast/GQLTypeDefinition; From a64e0b62af7fc4551c52ac1e000b7801a52cfd7f Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 15 Jul 2022 11:07:24 +0200 Subject: [PATCH 04/13] Make connectionTypes @ApolloInternal --- apollo-ast/api/apollo-ast.api | 1 - .../src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-ast/api/apollo-ast.api b/apollo-ast/api/apollo-ast.api index 90be08266d..98dc4045dc 100644 --- a/apollo-ast/api/apollo-ast.api +++ b/apollo-ast/api/apollo-ast.api @@ -1121,7 +1121,6 @@ public final class com/apollographql/apollo3/ast/Schema { public static final field REQUIRES_OPT_IN Ljava/lang/String; public static final field TYPE_POLICY Ljava/lang/String; public fun (Ljava/util/List;)V - public final fun getConnectionTypes ()Ljava/util/Set; public final fun getDirectiveDefinitions ()Ljava/util/Map; public final fun getForeignNames ()Ljava/util/Map; public final fun getMutationTypeDefinition ()Lcom/apollographql/apollo3/ast/GQLTypeDefinition; diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt index c53cfa98a7..cb4538b1f8 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/Schema.kt @@ -29,6 +29,7 @@ class Schema internal constructor( private val keyFields: Map>, val foreignNames: Map, private val directivesToStrip: List, + @ApolloInternal val connectionTypes: Set, ) { /** From f3a95a3444ff396b814bfe7f8ce5b620173ce9cd Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 15 Jul 2022 15:29:56 +0200 Subject: [PATCH 05/13] Add a warning when using a directive argument that has @requiresOptIn, and add it to @typePolicy.connectionFields --- .../ast/internal/SchemaValidationScope.kt | 36 +++++++++++++++++-- apollo-ast/src/main/resources/apollo.graphqls | 6 +++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt index e06e553a21..1068e44170 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt @@ -29,6 +29,7 @@ import com.apollographql.apollo3.ast.GQLTypeSystemExtension import com.apollographql.apollo3.ast.GQLUnionTypeDefinition import com.apollographql.apollo3.ast.Issue import com.apollographql.apollo3.ast.Schema +import com.apollographql.apollo3.ast.Schema.Companion.REQUIRES_OPT_IN import com.apollographql.apollo3.ast.Schema.Companion.TYPE_POLICY import com.apollographql.apollo3.ast.SourceLocation import com.apollographql.apollo3.ast.apolloDefinitions @@ -170,6 +171,8 @@ internal fun validateSchema(definitions: List, requiresApolloDefi val keyFields = mergedScope.validateAndComputeKeyFields() val connectionTypes = mergedScope.computeConnectionTypes() + mergedScope.validateRequiresOptInOnDirectiveArguments() + return if (issues.containsError()) { /** * Schema requires a valid Query root type which might not be always the case if there are error @@ -525,7 +528,7 @@ internal fun ValidationScope.validateAndComputeKeyFields(): Map { val connectionTypes = mutableSetOf() - for (typeDefinition in typeDefinitions.values) { + for (typeDefinition in typeDefinitions.values.filter { it is GQLObjectTypeDefinition || it is GQLInterfaceTypeDefinition }) { val connectionFields = typeDefinition.directives.filter { originalDirectiveName(it.name) == TYPE_POLICY }.toConnectionFields() for (fieldName in connectionFields) { val field = typeDefinition.fields.firstOrNull { it.name == fieldName } ?: continue @@ -535,11 +538,40 @@ internal fun ValidationScope.computeConnectionTypes(): Set { return connectionTypes } +internal fun ValidationScope.validateRequiresOptInOnDirectiveArguments() { + val directiveArgumentsRequiringOptIn = mutableMapOf, String>() // Directive+Argument -> Feature + for (directiveDefinition in directiveDefinitions.values) { + for (argument in directiveDefinition.arguments) { + val requiresOptInFeature = argument.directives.firstOrNull { originalDirectiveName(it.name) == REQUIRES_OPT_IN }?.arguments?.arguments?.firstOrNull { + it.name == "feature" + }?.value as? GQLStringValue ?: continue + directiveArgumentsRequiringOptIn[directiveDefinition.name to argument.name] = requiresOptInFeature.value + } + } + + for (typeDefinition in typeDefinitions.values) { + for (directive in typeDefinition.directives) { + val arguments = directive.arguments?.arguments ?: emptyList() + for (argument in arguments) { + val requiredFeatureForArgument = directiveArgumentsRequiringOptIn[directive.name to argument.name] ?: continue + registerIssue( + message = "'${directive.name}' directive argument '${argument.name}' requires opt-in feature '$requiredFeatureForArgument'", + sourceLocation = directive.sourceLocation, + severity = Issue.Severity.WARNING, + ) + } + } + } +} + private val GQLTypeDefinition.directives get() = when (this) { is GQLObjectTypeDefinition -> directives is GQLInterfaceTypeDefinition -> directives - else -> emptyList() + is GQLEnumTypeDefinition -> directives + is GQLInputObjectTypeDefinition -> directives + is GQLScalarTypeDefinition -> directives + is GQLUnionTypeDefinition -> directives } private val GQLTypeDefinition.fields diff --git a/apollo-ast/src/main/resources/apollo.graphqls b/apollo-ast/src/main/resources/apollo.graphqls index fd19ba7f61..9881f084d3 100644 --- a/apollo-ast/src/main/resources/apollo.graphqls +++ b/apollo-ast/src/main/resources/apollo.graphqls @@ -23,7 +23,11 @@ directive @nonnull(fields: String! = "") on OBJECT | FIELD # `embeddedFields`: a selection set containing fields that shouldn't create a new cache Record and should be # embedded in their parent instead. Order is unimportant. # `connectionFields`: a selection set containing fields that should be treated as Relay Connection fields. Order is unimportant. -directive @typePolicy(keyFields: String! = "", embeddedFields: String! = "", connectionFields: String! = "") on OBJECT | INTERFACE | UNION +directive @typePolicy( + keyFields: String! = "", + embeddedFields: String! = "", + connectionFields: String! = "" @requiresOptIn(feature: "connectionFields"), +) on OBJECT | INTERFACE | UNION # Attach extra information to a given field # `keyArgs`: a list of arguments used to compute the cache key of the object this field is pointing to. From 1ee57484ebc5a4092ece8eba9d494e4bdfc927a6 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 15 Jul 2022 15:57:00 +0200 Subject: [PATCH 06/13] Revert "Add a warning when using a directive argument that has @requiresOptIn, and add it to @typePolicy.connectionFields" This reverts commit f3a95a3444ff396b814bfe7f8ce5b620173ce9cd. --- .../ast/internal/SchemaValidationScope.kt | 36 ++----------------- apollo-ast/src/main/resources/apollo.graphqls | 6 +--- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt index 1068e44170..e06e553a21 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt @@ -29,7 +29,6 @@ import com.apollographql.apollo3.ast.GQLTypeSystemExtension import com.apollographql.apollo3.ast.GQLUnionTypeDefinition import com.apollographql.apollo3.ast.Issue import com.apollographql.apollo3.ast.Schema -import com.apollographql.apollo3.ast.Schema.Companion.REQUIRES_OPT_IN import com.apollographql.apollo3.ast.Schema.Companion.TYPE_POLICY import com.apollographql.apollo3.ast.SourceLocation import com.apollographql.apollo3.ast.apolloDefinitions @@ -171,8 +170,6 @@ internal fun validateSchema(definitions: List, requiresApolloDefi val keyFields = mergedScope.validateAndComputeKeyFields() val connectionTypes = mergedScope.computeConnectionTypes() - mergedScope.validateRequiresOptInOnDirectiveArguments() - return if (issues.containsError()) { /** * Schema requires a valid Query root type which might not be always the case if there are error @@ -528,7 +525,7 @@ internal fun ValidationScope.validateAndComputeKeyFields(): Map { val connectionTypes = mutableSetOf() - for (typeDefinition in typeDefinitions.values.filter { it is GQLObjectTypeDefinition || it is GQLInterfaceTypeDefinition }) { + for (typeDefinition in typeDefinitions.values) { val connectionFields = typeDefinition.directives.filter { originalDirectiveName(it.name) == TYPE_POLICY }.toConnectionFields() for (fieldName in connectionFields) { val field = typeDefinition.fields.firstOrNull { it.name == fieldName } ?: continue @@ -538,40 +535,11 @@ internal fun ValidationScope.computeConnectionTypes(): Set { return connectionTypes } -internal fun ValidationScope.validateRequiresOptInOnDirectiveArguments() { - val directiveArgumentsRequiringOptIn = mutableMapOf, String>() // Directive+Argument -> Feature - for (directiveDefinition in directiveDefinitions.values) { - for (argument in directiveDefinition.arguments) { - val requiresOptInFeature = argument.directives.firstOrNull { originalDirectiveName(it.name) == REQUIRES_OPT_IN }?.arguments?.arguments?.firstOrNull { - it.name == "feature" - }?.value as? GQLStringValue ?: continue - directiveArgumentsRequiringOptIn[directiveDefinition.name to argument.name] = requiresOptInFeature.value - } - } - - for (typeDefinition in typeDefinitions.values) { - for (directive in typeDefinition.directives) { - val arguments = directive.arguments?.arguments ?: emptyList() - for (argument in arguments) { - val requiredFeatureForArgument = directiveArgumentsRequiringOptIn[directive.name to argument.name] ?: continue - registerIssue( - message = "'${directive.name}' directive argument '${argument.name}' requires opt-in feature '$requiredFeatureForArgument'", - sourceLocation = directive.sourceLocation, - severity = Issue.Severity.WARNING, - ) - } - } - } -} - private val GQLTypeDefinition.directives get() = when (this) { is GQLObjectTypeDefinition -> directives is GQLInterfaceTypeDefinition -> directives - is GQLEnumTypeDefinition -> directives - is GQLInputObjectTypeDefinition -> directives - is GQLScalarTypeDefinition -> directives - is GQLUnionTypeDefinition -> directives + else -> emptyList() } private val GQLTypeDefinition.fields diff --git a/apollo-ast/src/main/resources/apollo.graphqls b/apollo-ast/src/main/resources/apollo.graphqls index 9881f084d3..fd19ba7f61 100644 --- a/apollo-ast/src/main/resources/apollo.graphqls +++ b/apollo-ast/src/main/resources/apollo.graphqls @@ -23,11 +23,7 @@ directive @nonnull(fields: String! = "") on OBJECT | FIELD # `embeddedFields`: a selection set containing fields that shouldn't create a new cache Record and should be # embedded in their parent instead. Order is unimportant. # `connectionFields`: a selection set containing fields that should be treated as Relay Connection fields. Order is unimportant. -directive @typePolicy( - keyFields: String! = "", - embeddedFields: String! = "", - connectionFields: String! = "" @requiresOptIn(feature: "connectionFields"), -) on OBJECT | INTERFACE | UNION +directive @typePolicy(keyFields: String! = "", embeddedFields: String! = "", connectionFields: String! = "") on OBJECT | INTERFACE | UNION # Attach extra information to a given field # `keyArgs`: a list of arguments used to compute the cache key of the object this field is pointing to. From aa70d65e037c11f1e8b90b0d03082a7ecbe6f01d Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 15 Jul 2022 16:36:57 +0200 Subject: [PATCH 07/13] Add v0.2 of the kotlin_labs directives (v0.1 is the default one when no import is specified). `connectionFields` is on v0.2 only and made opt-in that way --- .../apollographql/apollo3/ast/gqldocument.kt | 6 +-- .../ast/internal/SchemaValidationScope.kt | 5 +- .../src/main/resources/apollo-v0.1.graphqls | 50 +++++++++++++++++++ .../{apollo.graphqls => apollo-v0.2.graphqls} | 6 +-- .../graphql/pagination/schema.graphqls | 2 + 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 apollo-ast/src/main/resources/apollo-v0.1.graphqls rename apollo-ast/src/main/resources/{apollo.graphqls => apollo-v0.2.graphqls} (94%) diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt index 986e345903..d6f027a064 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt @@ -28,7 +28,7 @@ fun GQLDocument.withoutBuiltinDirectives(): GQLDocument { @Deprecated("This method is deprecated and will be removed in a future version") fun GQLDocument.withApolloDefinitions(): GQLDocument { @Suppress("DEPRECATION") - return withDefinitions(apolloDefinitions()) + return withDefinitions(apolloDefinitions("v0.1")) } /** @@ -44,9 +44,9 @@ fun builtinDefinitions() = definitionsFromResources("builtins.graphqls") fun linkDefinitions() = definitionsFromResources("link.graphqls") /** - * Extra apollo specific definitions from https://specs.apollo.dev/kotlin_labs/v0.1 + * Extra apollo specific definitions from https://specs.apollo.dev/kotlin_labs/<[version]> */ -fun apolloDefinitions() = definitionsFromResources("apollo.graphqls") +fun apolloDefinitions(version: String) = definitionsFromResources("apollo-$version.graphqls") private fun definitionsFromResources(name: String): List { return GQLDocument::class.java.getResourceAsStream("/$name")!! diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt index e06e553a21..6f99795da3 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt @@ -54,7 +54,7 @@ internal fun validateSchema(definitions: List, requiresApolloDefi var directivesToStrip = foreignSchemas.flatMap { it.directivesToStrip } - val apolloDefinitions = apolloDefinitions() + val apolloDefinitions = apolloDefinitions("v0.1") if (requiresApolloDefinitions && foreignSchemas.none { it.name == "kotlin_labs" }) { /** @@ -283,6 +283,7 @@ private fun List.getForeignSchemas( } val foreignName = components[components.size - 2] + val version = components[components.size - 1] if (prefix == null) { prefix = foreignName @@ -320,7 +321,7 @@ private fun List.getForeignSchemas( if (foreignName == "kotlin_labs") { - val (definitions, renames) = apolloDefinitions().rename(mappings, prefix) + val (definitions, renames) = apolloDefinitions(version).rename(mappings, prefix) foreignSchemas.add( ForeignSchema( name = foreignName, diff --git a/apollo-ast/src/main/resources/apollo-v0.1.graphqls b/apollo-ast/src/main/resources/apollo-v0.1.graphqls new file mode 100644 index 0000000000..6ac9cde8a2 --- /dev/null +++ b/apollo-ast/src/main/resources/apollo-v0.1.graphqls @@ -0,0 +1,50 @@ +# The kotlin_labs v0.1 directives +# You can import them with: +# +# extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.1", import: ["@optional", "@nonnull", "@typePolicy", "@fieldPolicy", "requiresOptIn", "targetName"]) +# +# They are included unconditionally for historical reasons but this will change in a future version + +# Marks a field or variable definition as optional or required +# By default Apollo Kotlin generates all variables of nullable types as optional, in compliance with the GraphQL specification, +# but this can be configured with this directive, because if the variable was added in the first place, it's usually to pass a value +directive @optional(if: Boolean = true) on FIELD | VARIABLE_DEFINITION + +# Marks a field as non-null. The corresponding Kotlin property will be made non-nullable even if the GraphQL type is nullable. +# When used on an object definition in a schema document, `fields` must be non-empty and contain a selection set of fields that should be non-null +# When used on a field from an executable document, `fields` must always be empty +# +# Setting the directive at the schema level is usually easier as there is little reason that a field would be non-null in one place +# and null in the other +directive @nonnull(fields: String! = "") on OBJECT | FIELD + +# Attach extra information to a given type +# `keyFields`: a selection set containing fields used to compute the cache key of an object. Order is important. +# `embeddedFields`: a selection set containing fields that shouldn't create a new cache Record and should be +# embedded in their parent instead. Order is unimportant. +directive @typePolicy(keyFields: String! = "", embeddedFields: String! = "") on OBJECT | INTERFACE | UNION + +# Attach extra information to a given field +# `keyArgs`: a list of arguments used to compute the cache key of the object this field is pointing to. +# The list is parsed as a selection set: both spaces and comas are valid separators. +# `paginationArgs` (experimental): a list of arguments that vary when requesting different pages. +# These arguments are omitted when computing the cache key of this field. +# The list is parsed as a selection set: both spaces and comas are valid separators. +directive @fieldPolicy(forField: String!, keyArgs: String! = "", paginationArgs: String! = "") repeatable on OBJECT + +""" +Indicates that the given field, argument, input field or enum value requires +giving explicit consent before being used. +""" +directive @requiresOptIn(feature: String!) repeatable +on FIELD_DEFINITION + | ARGUMENT_DEFINITION + | INPUT_FIELD_DEFINITION + | ENUM_VALUE + +# Use the specified name in the generated code instead of the GraphQL name. +# Use this for instance when the name would clash with a reserved keyword or field in the generated code. +# This directive is experimental. +directive @targetName(name: String!) +on OBJECT + | ENUM_VALUE diff --git a/apollo-ast/src/main/resources/apollo.graphqls b/apollo-ast/src/main/resources/apollo-v0.2.graphqls similarity index 94% rename from apollo-ast/src/main/resources/apollo.graphqls rename to apollo-ast/src/main/resources/apollo-v0.2.graphqls index fd19ba7f61..dc626f72ec 100644 --- a/apollo-ast/src/main/resources/apollo.graphqls +++ b/apollo-ast/src/main/resources/apollo-v0.2.graphqls @@ -1,7 +1,7 @@ -# The kotlin_labs directives -# You can import them with +# The kotlin_labs v0.2 directives +# You can import them with: # -# extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.1", import: ["@optional", "@nonnull", "@typePolicy", "@fieldPolicy"]) +# extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.2", import: ["@optional", "@nonnull", "@typePolicy", "@fieldPolicy", "requiresOptIn", "targetName"]) # # They are included unconditionally for historical reasons but this will change in a future version diff --git a/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls b/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls index 13159f9835..12e42ee8d4 100644 --- a/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls +++ b/tests/pagination/src/commonMain/graphql/pagination/schema.graphqls @@ -1,3 +1,5 @@ +extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.2", import: ["@typePolicy", "@fieldPolicy"]) + type Query @typePolicy(embeddedFields: "usersCursorBased, usersOffsetBasedWithPage" connectionFields: "usersCursorBased2, usersCursorBased3") @fieldPolicy(forField: "usersCursorBased", paginationArgs: "first, after, last, before") From 44ce18d294be7db6c9c7f303eada99045c80fae5 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 15 Jul 2022 17:01:36 +0200 Subject: [PATCH 08/13] Keep apolloDefinitions() --- .../main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt | 5 +++-- .../apollo3/ast/internal/SchemaValidationScope.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt index d6f027a064..3b6e5559f9 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt @@ -28,7 +28,7 @@ fun GQLDocument.withoutBuiltinDirectives(): GQLDocument { @Deprecated("This method is deprecated and will be removed in a future version") fun GQLDocument.withApolloDefinitions(): GQLDocument { @Suppress("DEPRECATION") - return withDefinitions(apolloDefinitions("v0.1")) + return withDefinitions(apolloDefinitions()) } /** @@ -46,7 +46,8 @@ fun linkDefinitions() = definitionsFromResources("link.graphqls") /** * Extra apollo specific definitions from https://specs.apollo.dev/kotlin_labs/<[version]> */ -fun apolloDefinitions(version: String) = definitionsFromResources("apollo-$version.graphqls") +fun apolloDefinitions() = apolloDefinitions("v0.1") +internal fun apolloDefinitions(version: String) = definitionsFromResources("apollo-$version.graphqls") private fun definitionsFromResources(name: String): List { return GQLDocument::class.java.getResourceAsStream("/$name")!! diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt index 6f99795da3..44787ceeaf 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt @@ -54,7 +54,7 @@ internal fun validateSchema(definitions: List, requiresApolloDefi var directivesToStrip = foreignSchemas.flatMap { it.directivesToStrip } - val apolloDefinitions = apolloDefinitions("v0.1") + val apolloDefinitions = apolloDefinitions() if (requiresApolloDefinitions && foreignSchemas.none { it.name == "kotlin_labs" }) { /** From 1505f22c780057444138d3f776c4d0ffcc8a6756 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 15 Jul 2022 17:12:29 +0200 Subject: [PATCH 09/13] Fix validation test --- .../src/test/validation/schema/apollo_directive.expected | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apollo-compiler/src/test/validation/schema/apollo_directive.expected b/apollo-compiler/src/test/validation/schema/apollo_directive.expected index a707a87445..b8971b1bd3 100644 --- a/apollo-compiler/src/test/validation/schema/apollo_directive.expected +++ b/apollo-compiler/src/test/validation/schema/apollo_directive.expected @@ -1,2 +1,2 @@ -WARNING: ValidationError (6:0) -Directive 'nonnull' is defined multiple times. First definition is: (apollo.graphqls): (19, 1) \ No newline at end of file +WARNING: ValidationError(6:0) +Directive 'nonnull' is defined multiple times . First definition is : (apollo - v0.1.graphqls): (19, 1) \ No newline at end of file From 6e5f77da355f1f6720740e387a94ec1727fc0a47 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 15 Jul 2022 18:23:26 +0200 Subject: [PATCH 10/13] Fix bad formatting in the .expected --- .../src/test/validation/schema/apollo_directive.expected | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apollo-compiler/src/test/validation/schema/apollo_directive.expected b/apollo-compiler/src/test/validation/schema/apollo_directive.expected index b8971b1bd3..88d7cba38d 100644 --- a/apollo-compiler/src/test/validation/schema/apollo_directive.expected +++ b/apollo-compiler/src/test/validation/schema/apollo_directive.expected @@ -1,2 +1,2 @@ -WARNING: ValidationError(6:0) -Directive 'nonnull' is defined multiple times . First definition is : (apollo - v0.1.graphqls): (19, 1) \ No newline at end of file +WARNING: ValidationError (6:0) +Directive 'nonnull' is defined multiple times. First definition is: (apollo-v0.1.graphqls): (19, 1) \ No newline at end of file From 6591785d375a65b4c2cb0f1f049d0ed8326b2a79 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Mon, 18 Jul 2022 12:00:35 +0200 Subject: [PATCH 11/13] try to document the process of evolving the directives (#4275) --- apollo-ast/src/main/resources/README.md | 6 ++ .../src/main/resources/apollo-v0.1.graphqls | 7 -- .../src/main/resources/apollo-v0.2.graphqls | 93 ++++++++++++------- 3 files changed, 67 insertions(+), 39 deletions(-) create mode 100644 apollo-ast/src/main/resources/README.md diff --git a/apollo-ast/src/main/resources/README.md b/apollo-ast/src/main/resources/README.md new file mode 100644 index 0000000000..d936dbc2a2 --- /dev/null +++ b/apollo-ast/src/main/resources/README.md @@ -0,0 +1,6 @@ +This folder contains several groups of GraphQL definitions we use during codegen: + +* builtins.graphqls: the official built-in defintions such as [built-in scalars](https://spec.graphql.org/draft/#sec-Scalars.Built-in-Scalars), [built-in directives](https://spec.graphql.org/draft/#sec-Type-System.Directives.Built-in-Directives) or [introspection definitions](https://spec.graphql.org/draft/#sec-Schema-Introspection.Schema-Introspection-Schema). +* link.graphqls: the [core schemas](https://specs.apollo.dev/link/v1.0/) definitions. +* apollo-${version}.graphqls: the client directives supported by Apollo Kotlin. Changes are versioned at https://github.com/apollographql/specs. Changing them requires a new version and a PR. + diff --git a/apollo-ast/src/main/resources/apollo-v0.1.graphqls b/apollo-ast/src/main/resources/apollo-v0.1.graphqls index 6ac9cde8a2..40c09f1148 100644 --- a/apollo-ast/src/main/resources/apollo-v0.1.graphqls +++ b/apollo-ast/src/main/resources/apollo-v0.1.graphqls @@ -1,10 +1,3 @@ -# The kotlin_labs v0.1 directives -# You can import them with: -# -# extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.1", import: ["@optional", "@nonnull", "@typePolicy", "@fieldPolicy", "requiresOptIn", "targetName"]) -# -# They are included unconditionally for historical reasons but this will change in a future version - # Marks a field or variable definition as optional or required # By default Apollo Kotlin generates all variables of nullable types as optional, in compliance with the GraphQL specification, # but this can be configured with this directive, because if the variable was added in the first place, it's usually to pass a value diff --git a/apollo-ast/src/main/resources/apollo-v0.2.graphqls b/apollo-ast/src/main/resources/apollo-v0.2.graphqls index dc626f72ec..a917566e02 100644 --- a/apollo-ast/src/main/resources/apollo-v0.2.graphqls +++ b/apollo-ast/src/main/resources/apollo-v0.2.graphqls @@ -1,41 +1,67 @@ -# The kotlin_labs v0.2 directives -# You can import them with: -# -# extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.2", import: ["@optional", "@nonnull", "@typePolicy", "@fieldPolicy", "requiresOptIn", "targetName"]) -# -# They are included unconditionally for historical reasons but this will change in a future version - -# Marks a field or variable definition as optional or required -# By default Apollo Kotlin generates all variables of nullable types as optional, in compliance with the GraphQL specification, -# but this can be configured with this directive, because if the variable was added in the first place, it's usually to pass a value +""" +Marks a field or variable definition as optional or required +By default Apollo Kotlin generates all variables of nullable types as optional, in compliance with the GraphQL specification, +but this can be configured with this directive, because if the variable was added in the first place, it's usually to pass a value +Since: 3.0.0 +""" directive @optional(if: Boolean = true) on FIELD | VARIABLE_DEFINITION -# Marks a field as non-null. The corresponding Kotlin property will be made non-nullable even if the GraphQL type is nullable. -# When used on an object definition in a schema document, `fields` must be non-empty and contain a selection set of fields that should be non-null -# When used on a field from an executable document, `fields` must always be empty -# -# Setting the directive at the schema level is usually easier as there is little reason that a field would be non-null in one place -# and null in the other +""" +Marks a field as non-null. The corresponding Kotlin property will be made non-nullable even if the GraphQL type is nullable. +When used on an object definition in a schema document, `fields` must be non-empty and contain a selection set of fields that should be non-null +When used on a field from an executable document, `fields` must always be empty + +Setting the directive at the schema level is usually easier as there is little reason that a field would be non-null in one place +and null in the other +Since: 3.0.0 +""" directive @nonnull(fields: String! = "") on OBJECT | FIELD -# Attach extra information to a given type -# `keyFields`: a selection set containing fields used to compute the cache key of an object. Order is important. -# `embeddedFields`: a selection set containing fields that shouldn't create a new cache Record and should be -# embedded in their parent instead. Order is unimportant. -# `connectionFields`: a selection set containing fields that should be treated as Relay Connection fields. Order is unimportant. -directive @typePolicy(keyFields: String! = "", embeddedFields: String! = "", connectionFields: String! = "") on OBJECT | INTERFACE | UNION +""" +Attach extra information to a given type +Since: 3.0.0 +""" +directive @typePolicy( + """ + a selection set containing fields used to compute the cache key of an object. Order is important. + """ + keyFields: String! = "", + """ + a selection set containing fields that shouldn't create a new cache Record and should be + embedded in their parent instead. Order is unimportant. + """ + embeddedFields: String! = "", + """ + a selection set containing fields that should be treated as Relay Connection fields. Order is unimportant. + Since: 3.4.1 + """ + connectionFields: String! = "" +) on OBJECT | INTERFACE | UNION -# Attach extra information to a given field -# `keyArgs`: a list of arguments used to compute the cache key of the object this field is pointing to. -# The list is parsed as a selection set: both spaces and comas are valid separators. -# `paginationArgs` (experimental): a list of arguments that vary when requesting different pages. -# These arguments are omitted when computing the cache key of this field. -# The list is parsed as a selection set: both spaces and comas are valid separators. -directive @fieldPolicy(forField: String!, keyArgs: String! = "", paginationArgs: String! = "") repeatable on OBJECT +""" +Attach extra information to a given field +Since: 3.3.0 +""" +directive @fieldPolicy( + forField: String!, + """ + a list of arguments used to compute the cache key of the object this field is pointing to. + The list is parsed as a selection set: both spaces and comas are valid separators. + """ + keyArgs: String! = "", + """ + (experimental) a list of arguments that vary when requesting different pages. + These arguments are omitted when computing the cache key of this field. + The list is parsed as a selection set: both spaces and comas are valid separators. + Since: 3.4.1 + """ + paginationArgs: String! = "" +) repeatable on OBJECT """ Indicates that the given field, argument, input field or enum value requires giving explicit consent before being used. +Since: 3.3.1 """ directive @requiresOptIn(feature: String!) repeatable on FIELD_DEFINITION @@ -43,9 +69,12 @@ on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE -# Use the specified name in the generated code instead of the GraphQL name. -# Use this for instance when the name would clash with a reserved keyword or field in the generated code. -# This directive is experimental. +""" +Use the specified name in the generated code instead of the GraphQL name. +Use this for instance when the name would clash with a reserved keyword or field in the generated code. +This directive is experimental. +Since: 3.3.1 +""" directive @targetName(name: String!) on OBJECT | INTERFACE From dd1eee84377c5257dc50fab423d0565e51ccce83 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 18 Jul 2022 12:07:22 +0200 Subject: [PATCH 12/13] Make apolloDefinitions(version) public and deprecate apolloDefinitions() --- apollo-ast/api/apollo-ast.api | 1 + .../kotlin/com/apollographql/apollo3/ast/gqldocument.kt | 6 ++++-- .../apollo3/ast/internal/SchemaValidationScope.kt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apollo-ast/api/apollo-ast.api b/apollo-ast/api/apollo-ast.api index 98dc4045dc..cd212cae75 100644 --- a/apollo-ast/api/apollo-ast.api +++ b/apollo-ast/api/apollo-ast.api @@ -982,6 +982,7 @@ public final class com/apollographql/apollo3/ast/GqldirectiveKt { public final class com/apollographql/apollo3/ast/GqldocumentKt { public static final fun apolloDefinitions ()Ljava/util/List; + public static final fun apolloDefinitions (Ljava/lang/String;)Ljava/util/List; public static final fun builtinDefinitions ()Ljava/util/List; public static final fun linkDefinitions ()Ljava/util/List; public static final fun withApolloDefinitions (Lcom/apollographql/apollo3/ast/GQLDocument;)Lcom/apollographql/apollo3/ast/GQLDocument; diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt index 3b6e5559f9..66aee01fa8 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/gqldocument.kt @@ -43,11 +43,13 @@ fun builtinDefinitions() = definitionsFromResources("builtins.graphqls") */ fun linkDefinitions() = definitionsFromResources("link.graphqls") +@Deprecated("Use apolloDefinitions(version) instead", ReplaceWith("apolloDefinitions(\"v0.1\")")) +fun apolloDefinitions() = apolloDefinitions("v0.1") + /** * Extra apollo specific definitions from https://specs.apollo.dev/kotlin_labs/<[version]> */ -fun apolloDefinitions() = apolloDefinitions("v0.1") -internal fun apolloDefinitions(version: String) = definitionsFromResources("apollo-$version.graphqls") +fun apolloDefinitions(version: String) = definitionsFromResources("apollo-$version.graphqls") private fun definitionsFromResources(name: String): List { return GQLDocument::class.java.getResourceAsStream("/$name")!! diff --git a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt index 44787ceeaf..6f99795da3 100644 --- a/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt +++ b/apollo-ast/src/main/kotlin/com/apollographql/apollo3/ast/internal/SchemaValidationScope.kt @@ -54,7 +54,7 @@ internal fun validateSchema(definitions: List, requiresApolloDefi var directivesToStrip = foreignSchemas.flatMap { it.directivesToStrip } - val apolloDefinitions = apolloDefinitions() + val apolloDefinitions = apolloDefinitions("v0.1") if (requiresApolloDefinitions && foreignSchemas.none { it.name == "kotlin_labs" }) { /** From 51300ab86452cdd87756264790b15455f3a7bae5 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 18 Jul 2022 12:48:04 +0200 Subject: [PATCH 13/13] Fix validation test --- .../src/test/validation/schema/apollo_directive.expected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-compiler/src/test/validation/schema/apollo_directive.expected b/apollo-compiler/src/test/validation/schema/apollo_directive.expected index 88d7cba38d..ac0e980a48 100644 --- a/apollo-compiler/src/test/validation/schema/apollo_directive.expected +++ b/apollo-compiler/src/test/validation/schema/apollo_directive.expected @@ -1,2 +1,2 @@ WARNING: ValidationError (6:0) -Directive 'nonnull' is defined multiple times. First definition is: (apollo-v0.1.graphqls): (19, 1) \ No newline at end of file +Directive 'nonnull' is defined multiple times. First definition is: (apollo-v0.1.graphqls): (12, 1) \ No newline at end of file