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

Pagination: add connectionFields argument to @typePolicy #4265

Merged
merged 13 commits into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Schema internal constructor(
private val keyFields: Map<String, Set<String>>,
val foreignNames: Map<String, String>,
private val directivesToStrip: List<String>,
@ApolloInternal
val connectionTypes: Set<String>,
) {
/**
* Creates a new Schema from a list of definition.
Expand All @@ -42,7 +44,8 @@ class Schema internal constructor(
definitions,
emptyMap(),
emptyMap(),
emptyList()
emptyList(),
emptySet(),
)

val typeDefinitions: Map<String, GQLTypeDefinition> = definitions
Expand Down Expand Up @@ -137,7 +140,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,
)
}

Expand Down Expand Up @@ -213,6 +217,7 @@ class Schema internal constructor(
keyFields = (map["keyFields"]!! as Map<String, Collection<String>>).mapValues { it.value.toSet() },
foreignNames = map["foreignNames"]!! as Map<String, String>,
directivesToStrip = map["directivesToStrip"]!! as List<String>,
connectionTypes = (map["connectionTypes"]!! as List<String>).toSet(),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ 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() = apolloDefinitions("v0.1")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make Deprecated?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably make apolloDefinitions(version: String) public if we want to keep offering that possibility. I'd be surprised if anyone is relying on this at the moment but it might be useful for some use cases?

internal fun apolloDefinitions(version: String) = definitionsFromResources("apollo-$version.graphqls")

private fun definitionsFromResources(name: String): List<GQLDefinition> {
return GQLDocument::class.java.getResourceAsStream("/$name")!!
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GQLDefinition>, requiresApolloDefinitions: Boolean = false): GQLResult<Schema> {
val issues = mutableListOf<Issue>()
Expand Down Expand Up @@ -133,6 +168,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
mergedScope.validateObjects()

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

return if (issues.containsError()) {
/**
Expand All @@ -142,10 +178,11 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
} else {
GQLResult(
Schema(
mergedDefinitions,
keyFields,
foreignNames,
directivesToStrip
definitions = mergedDefinitions,
keyFields = keyFields,
foreignNames = foreignNames,
directivesToStrip = directivesToStrip,
connectionTypes = connectionTypes,
),
issues
)
Expand Down Expand Up @@ -246,6 +283,7 @@ private fun List<GQLSchemaExtension>.getForeignSchemas(
}

val foreignName = components[components.size - 2]
val version = components[components.size - 1]

if (prefix == null) {
prefix = foreignName
Expand Down Expand Up @@ -283,7 +321,7 @@ private fun List<GQLSchemaExtension>.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,
Expand Down Expand Up @@ -452,6 +490,9 @@ private fun List<GQLDirective>.toKeyFields(): Set<String> = extractFields("keyFi
@ApolloInternal
fun List<GQLDirective>.toEmbeddedFields(): Set<String> = extractFields("embeddedFields")

@ApolloInternal
fun List<GQLDirective>.toConnectionFields(): Set<String> = extractFields("connectionFields")

private fun List<GQLDirective>.extractFields(argumentName: String): Set<String> {
if (isEmpty()) {
return emptySet()
Expand Down Expand Up @@ -481,4 +522,37 @@ internal fun ValidationScope.validateAndComputeKeyFields(): Map<String, Set<Stri
keyFields(it, keyFieldsCache)
}
return keyFieldsCache
}
}

internal fun ValidationScope.computeConnectionTypes(): Set<String> {
val connectionTypes = mutableSetOf<String>()
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be on GQLTypeDefinition directly instead of an extension?

Copy link
Contributor

@martinbonnin martinbonnin Jul 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going that road, we could add a GQLHasDirectives interface like there is GQLNamed and GQLDescribed. I'm not 100% sure the implications of this though.

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
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# The kotlin_labs directives
# You can import them with
# 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"])
# extend schema @link(url: "https://specs.apollo.dev/kotlin_labs/v0.1", import: ["@optional", "@nonnull", "@typePolicy", "@fieldPolicy", "requiresOptIn", "targetName"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

#
# They are included unconditionally for historical reasons but this will change in a future version

Expand Down Expand Up @@ -47,9 +47,4 @@ on FIELD_DEFINITION
# This directive is experimental.
directive @targetName(name: String!)
on OBJECT
| INTERFACE
| ENUM
| ENUM_VALUE
| UNION
| SCALAR
| INPUT_OBJECT
56 changes: 56 additions & 0 deletions apollo-ast/src/main/resources/apollo-v0.2.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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
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.
# `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.
# 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
| INTERFACE
| ENUM
| ENUM_VALUE
| UNION
| SCALAR
| INPUT_OBJECT
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -18,15 +19,10 @@ internal fun GQLTypeDefinition.keyArgs(
internal fun GQLTypeDefinition.paginationArgs(
fieldName: String,
schema: Schema,
): Set<String> = fieldPolicyArgs(Schema.FIELD_POLICY_PAGINATION_ARGS, fieldName, schema)
): Set<String> = fieldPolicyArgs(Schema.FIELD_POLICY_PAGINATION_ARGS, fieldName, schema) +
typePolicyConnectionArgs(fieldName, schema)

private fun GQLTypeDefinition.fieldPolicyArgs(argumentName: String, fieldName: String, schema: Schema): Set<String> {
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 {
Expand All @@ -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<String> {
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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading