From 10ff13ab3e1afa2daaac86133bb6e6a0012f48a3 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 5 Dec 2025 17:00:55 +0100 Subject: [PATCH 1/3] Add support for directives on directive definitions --- libraries/apollo-ast/api/apollo-ast.api | 23 ++++- libraries/apollo-ast/api/apollo-ast.klib.api | 24 +++++- .../com/apollographql/apollo/ast/gql.kt | 85 ++++++++++++++++++- .../apollo/ast/internal/ExtensionsMerger.kt | 11 +++ .../apollo/ast/internal/Parser.kt | 23 ++++- .../ast/internal/SchemaValidationScope.kt | 5 +- .../apollo/ast/internal/ValidationCommon.kt | 2 + .../ast/introspection/introspection_reader.kt | 12 ++- .../com/apollographql/apollo/ast/locations.kt | 7 +- .../apollo/graphql/ast/test/SchemaTest.kt | 3 + .../test-fixtures/sdl/introspection.expected | 8 +- .../test-fixtures/sdl/simple.expected | 2 + .../test-fixtures/sdl/simple.graphqls | 4 +- .../schema/unexpected-definitions.expected | 2 +- ...trospection-response-success-full.graphqls | 8 +- tests/sample-server/graphql/schema.graphqls | 10 +-- 16 files changed, 202 insertions(+), 27 deletions(-) diff --git a/libraries/apollo-ast/api/apollo-ast.api b/libraries/apollo-ast/api/apollo-ast.api index eefe3bc586c..b87cc0f0dfd 100644 --- a/libraries/apollo-ast/api/apollo-ast.api +++ b/libraries/apollo-ast/api/apollo-ast.api @@ -231,16 +231,21 @@ public final class com/apollographql/apollo/ast/GQLDirectiveCoordinate : com/apo public fun writeInternal (Lcom/apollographql/apollo/ast/SDLWriter;)V } -public final class com/apollographql/apollo/ast/GQLDirectiveDefinition : com/apollographql/apollo/ast/GQLDefinition, com/apollographql/apollo/ast/GQLDescribed, com/apollographql/apollo/ast/GQLNamed { +public final class com/apollographql/apollo/ast/GQLDirectiveDefinition : com/apollographql/apollo/ast/GQLDefinition, com/apollographql/apollo/ast/GQLDescribed, com/apollographql/apollo/ast/GQLHasDirectives, com/apollographql/apollo/ast/GQLNamed { public static final field Companion Lcom/apollographql/apollo/ast/GQLDirectiveDefinition$Companion; public fun (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;)V public synthetic fun (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun copy (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;)Lcom/apollographql/apollo/ast/GQLDirectiveDefinition; + public fun (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;Ljava/util/List;)V + public synthetic fun (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final synthetic fun copy (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;)Lcom/apollographql/apollo/ast/GQLDirectiveDefinition; + public final fun copy (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;Ljava/util/List;)Lcom/apollographql/apollo/ast/GQLDirectiveDefinition; public static synthetic fun copy$default (Lcom/apollographql/apollo/ast/GQLDirectiveDefinition;Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;ILjava/lang/Object;)Lcom/apollographql/apollo/ast/GQLDirectiveDefinition; + public static synthetic fun copy$default (Lcom/apollographql/apollo/ast/GQLDirectiveDefinition;Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ZLjava/util/List;Ljava/util/List;ILjava/lang/Object;)Lcom/apollographql/apollo/ast/GQLDirectiveDefinition; public fun copyWithNewChildrenInternal (Lcom/apollographql/apollo/ast/NodeContainer;)Lcom/apollographql/apollo/ast/GQLNode; public final fun getArguments ()Ljava/util/List; public fun getChildren ()Ljava/util/List; public fun getDescription ()Ljava/lang/String; + public fun getDirectives ()Ljava/util/List; public final fun getLocations ()Ljava/util/List; public fun getName ()Ljava/lang/String; public final fun getRepeatable ()Z @@ -253,8 +258,22 @@ public final class com/apollographql/apollo/ast/GQLDirectiveDefinition$Companion public final fun getBuiltInDirectives ()Ljava/util/Set; } +public final class com/apollographql/apollo/ast/GQLDirectiveExtension : com/apollographql/apollo/ast/GQLDefinition, com/apollographql/apollo/ast/GQLHasDirectives, com/apollographql/apollo/ast/GQLNamed, com/apollographql/apollo/ast/GQLTypeSystemExtension { + public fun (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/util/List;)V + public synthetic fun (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun copy (Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/util/List;)Lcom/apollographql/apollo/ast/GQLDirectiveExtension; + public static synthetic fun copy$default (Lcom/apollographql/apollo/ast/GQLDirectiveExtension;Lcom/apollographql/apollo/ast/SourceLocation;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/apollographql/apollo/ast/GQLDirectiveExtension; + public fun copyWithNewChildrenInternal (Lcom/apollographql/apollo/ast/NodeContainer;)Lcom/apollographql/apollo/ast/GQLNode; + public fun getChildren ()Ljava/util/List; + public fun getDirectives ()Ljava/util/List; + public fun getName ()Ljava/lang/String; + public fun getSourceLocation ()Lcom/apollographql/apollo/ast/SourceLocation; + public fun writeInternal (Lcom/apollographql/apollo/ast/SDLWriter;)V +} + public final class com/apollographql/apollo/ast/GQLDirectiveLocation : java/lang/Enum { public static final field ARGUMENT_DEFINITION Lcom/apollographql/apollo/ast/GQLDirectiveLocation; + public static final field DIRECTIVE_DEFINITION Lcom/apollographql/apollo/ast/GQLDirectiveLocation; public static final field ENUM Lcom/apollographql/apollo/ast/GQLDirectiveLocation; public static final field ENUM_VALUE Lcom/apollographql/apollo/ast/GQLDirectiveLocation; public static final field FIELD Lcom/apollographql/apollo/ast/GQLDirectiveLocation; diff --git a/libraries/apollo-ast/api/apollo-ast.klib.api b/libraries/apollo-ast/api/apollo-ast.klib.api index 7d28d9a94f7..0d84541c74e 100644 --- a/libraries/apollo-ast/api/apollo-ast.klib.api +++ b/libraries/apollo-ast/api/apollo-ast.klib.api @@ -20,6 +20,7 @@ final enum class com.apollographql.apollo.ast/CatchTo : kotlin/Enum { // com.apollographql.apollo.ast/GQLDirectiveLocation|null[0] enum entry ARGUMENT_DEFINITION // com.apollographql.apollo.ast/GQLDirectiveLocation.ARGUMENT_DEFINITION|null[0] + enum entry DIRECTIVE_DEFINITION // com.apollographql.apollo.ast/GQLDirectiveLocation.DIRECTIVE_DEFINITION|null[0] enum entry ENUM // com.apollographql.apollo.ast/GQLDirectiveLocation.ENUM|null[0] enum entry ENUM_VALUE // com.apollographql.apollo.ast/GQLDirectiveLocation.ENUM_VALUE|null[0] enum entry FIELD // com.apollographql.apollo.ast/GQLDirectiveLocation.FIELD|null[0] @@ -362,8 +363,9 @@ final class com.apollographql.apollo.ast/GQLDirectiveCoordinate : com.apollograp final fun writeInternal(com.apollographql.apollo.ast/SDLWriter) // com.apollographql.apollo.ast/GQLDirectiveCoordinate.writeInternal|writeInternal(com.apollographql.apollo.ast.SDLWriter){}[0] } -final class com.apollographql.apollo.ast/GQLDirectiveDefinition : com.apollographql.apollo.ast/GQLDefinition, com.apollographql.apollo.ast/GQLDescribed, com.apollographql.apollo.ast/GQLNamed { // com.apollographql.apollo.ast/GQLDirectiveDefinition|null[0] +final class com.apollographql.apollo.ast/GQLDirectiveDefinition : com.apollographql.apollo.ast/GQLDefinition, com.apollographql.apollo.ast/GQLDescribed, com.apollographql.apollo.ast/GQLHasDirectives, com.apollographql.apollo.ast/GQLNamed { // com.apollographql.apollo.ast/GQLDirectiveDefinition|null[0] constructor (com.apollographql.apollo.ast/SourceLocation? = ..., kotlin/String?, kotlin/String, kotlin.collections/List, kotlin/Boolean, kotlin.collections/List) // com.apollographql.apollo.ast/GQLDirectiveDefinition.|(com.apollographql.apollo.ast.SourceLocation?;kotlin.String?;kotlin.String;kotlin.collections.List;kotlin.Boolean;kotlin.collections.List){}[0] + constructor (com.apollographql.apollo.ast/SourceLocation? = ..., kotlin/String?, kotlin/String, kotlin.collections/List, kotlin/Boolean, kotlin.collections/List, kotlin.collections/List) // com.apollographql.apollo.ast/GQLDirectiveDefinition.|(com.apollographql.apollo.ast.SourceLocation?;kotlin.String?;kotlin.String;kotlin.collections.List;kotlin.Boolean;kotlin.collections.List;kotlin.collections.List){}[0] final val arguments // com.apollographql.apollo.ast/GQLDirectiveDefinition.arguments|{}arguments[0] final fun (): kotlin.collections/List // com.apollographql.apollo.ast/GQLDirectiveDefinition.arguments.|(){}[0] @@ -371,6 +373,8 @@ final class com.apollographql.apollo.ast/GQLDirectiveDefinition : com.apollograp final fun (): kotlin.collections/List // com.apollographql.apollo.ast/GQLDirectiveDefinition.children.|(){}[0] final val description // com.apollographql.apollo.ast/GQLDirectiveDefinition.description|{}description[0] final fun (): kotlin/String? // com.apollographql.apollo.ast/GQLDirectiveDefinition.description.|(){}[0] + final val directives // com.apollographql.apollo.ast/GQLDirectiveDefinition.directives|{}directives[0] + final fun (): kotlin.collections/List // com.apollographql.apollo.ast/GQLDirectiveDefinition.directives.|(){}[0] final val locations // com.apollographql.apollo.ast/GQLDirectiveDefinition.locations|{}locations[0] final fun (): kotlin.collections/List // com.apollographql.apollo.ast/GQLDirectiveDefinition.locations.|(){}[0] final val name // com.apollographql.apollo.ast/GQLDirectiveDefinition.name|{}name[0] @@ -381,6 +385,7 @@ final class com.apollographql.apollo.ast/GQLDirectiveDefinition : com.apollograp final fun (): com.apollographql.apollo.ast/SourceLocation? // com.apollographql.apollo.ast/GQLDirectiveDefinition.sourceLocation.|(){}[0] final fun copy(com.apollographql.apollo.ast/SourceLocation? = ..., kotlin/String? = ..., kotlin/String = ..., kotlin.collections/List = ..., kotlin/Boolean = ..., kotlin.collections/List = ...): com.apollographql.apollo.ast/GQLDirectiveDefinition // com.apollographql.apollo.ast/GQLDirectiveDefinition.copy|copy(com.apollographql.apollo.ast.SourceLocation?;kotlin.String?;kotlin.String;kotlin.collections.List;kotlin.Boolean;kotlin.collections.List){}[0] + final fun copy(com.apollographql.apollo.ast/SourceLocation? = ..., kotlin/String? = ..., kotlin/String = ..., kotlin.collections/List = ..., kotlin/Boolean = ..., kotlin.collections/List = ..., kotlin.collections/List = ...): com.apollographql.apollo.ast/GQLDirectiveDefinition // com.apollographql.apollo.ast/GQLDirectiveDefinition.copy|copy(com.apollographql.apollo.ast.SourceLocation?;kotlin.String?;kotlin.String;kotlin.collections.List;kotlin.Boolean;kotlin.collections.List;kotlin.collections.List){}[0] final fun copyWithNewChildrenInternal(com.apollographql.apollo.ast/NodeContainer): com.apollographql.apollo.ast/GQLNode // com.apollographql.apollo.ast/GQLDirectiveDefinition.copyWithNewChildrenInternal|copyWithNewChildrenInternal(com.apollographql.apollo.ast.NodeContainer){}[0] final fun isBuiltIn(): kotlin/Boolean // com.apollographql.apollo.ast/GQLDirectiveDefinition.isBuiltIn|isBuiltIn(){}[0] final fun writeInternal(com.apollographql.apollo.ast/SDLWriter) // com.apollographql.apollo.ast/GQLDirectiveDefinition.writeInternal|writeInternal(com.apollographql.apollo.ast.SDLWriter){}[0] @@ -391,6 +396,23 @@ final class com.apollographql.apollo.ast/GQLDirectiveDefinition : com.apollograp } } +final class com.apollographql.apollo.ast/GQLDirectiveExtension : com.apollographql.apollo.ast/GQLDefinition, com.apollographql.apollo.ast/GQLHasDirectives, com.apollographql.apollo.ast/GQLNamed, com.apollographql.apollo.ast/GQLTypeSystemExtension { // com.apollographql.apollo.ast/GQLDirectiveExtension|null[0] + constructor (com.apollographql.apollo.ast/SourceLocation? = ..., kotlin/String, kotlin.collections/List) // com.apollographql.apollo.ast/GQLDirectiveExtension.|(com.apollographql.apollo.ast.SourceLocation?;kotlin.String;kotlin.collections.List){}[0] + + final val children // com.apollographql.apollo.ast/GQLDirectiveExtension.children|{}children[0] + final fun (): kotlin.collections/List // com.apollographql.apollo.ast/GQLDirectiveExtension.children.|(){}[0] + final val directives // com.apollographql.apollo.ast/GQLDirectiveExtension.directives|{}directives[0] + final fun (): kotlin.collections/List // com.apollographql.apollo.ast/GQLDirectiveExtension.directives.|(){}[0] + final val name // com.apollographql.apollo.ast/GQLDirectiveExtension.name|{}name[0] + final fun (): kotlin/String // com.apollographql.apollo.ast/GQLDirectiveExtension.name.|(){}[0] + final val sourceLocation // com.apollographql.apollo.ast/GQLDirectiveExtension.sourceLocation|{}sourceLocation[0] + final fun (): com.apollographql.apollo.ast/SourceLocation? // com.apollographql.apollo.ast/GQLDirectiveExtension.sourceLocation.|(){}[0] + + final fun copy(com.apollographql.apollo.ast/SourceLocation? = ..., kotlin/String = ..., kotlin.collections/List = ...): com.apollographql.apollo.ast/GQLDirectiveExtension // com.apollographql.apollo.ast/GQLDirectiveExtension.copy|copy(com.apollographql.apollo.ast.SourceLocation?;kotlin.String;kotlin.collections.List){}[0] + final fun copyWithNewChildrenInternal(com.apollographql.apollo.ast/NodeContainer): com.apollographql.apollo.ast/GQLNode // com.apollographql.apollo.ast/GQLDirectiveExtension.copyWithNewChildrenInternal|copyWithNewChildrenInternal(com.apollographql.apollo.ast.NodeContainer){}[0] + final fun writeInternal(com.apollographql.apollo.ast/SDLWriter) // com.apollographql.apollo.ast/GQLDirectiveExtension.writeInternal|writeInternal(com.apollographql.apollo.ast.SDLWriter){}[0] +} + final class com.apollographql.apollo.ast/GQLDocument : com.apollographql.apollo.ast/GQLNode { // com.apollographql.apollo.ast/GQLDocument|null[0] constructor (kotlin.collections/List, com.apollographql.apollo.ast/SourceLocation?) // com.apollographql.apollo.ast/GQLDocument.|(kotlin.collections.List;com.apollographql.apollo.ast.SourceLocation?){}[0] diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gql.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gql.kt index 06da2b15310..f56bb8e39de 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gql.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gql.kt @@ -678,7 +678,28 @@ class GQLDirectiveDefinition( val arguments: List, val repeatable: Boolean, val locations: List, -) : GQLDefinition, GQLDescribed, GQLNamed { + override val directives: List, +) : GQLDefinition, GQLDescribed, GQLNamed, GQLHasDirectives { + + @Deprecated("Use the other constructor") + @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) + constructor( + sourceLocation: SourceLocation? = null, + description: String?, + name: String, + arguments: List, + repeatable: Boolean, + locations: List, + ) : this( + sourceLocation = sourceLocation, + description = description, + name = name, + arguments = arguments, + repeatable = repeatable, + locations = locations, + directives = emptyList(), + ) + override val children: List = arguments override fun writeInternal(writer: SDLWriter) { @@ -686,15 +707,17 @@ class GQLDirectiveDefinition( writeDescription(description) write("directive @$name") if (arguments.isNotEmpty()) { - write(" ") arguments.join(writer, prefix = "(", separator = ", ", postfix = ")") { it.write(writer, true) } } + if (directives.isNotEmpty()) { + directives.join(writer, prefix = " ") + } if (repeatable) { write(" repeatable") } - write(" on ${locations.joinToString("|")}") + write(" on ${locations.joinToString(" | ")}") write("\n") } } @@ -706,6 +729,7 @@ class GQLDirectiveDefinition( arguments: List = this.arguments, repeatable: Boolean = this.repeatable, locations: List = this.locations, + directives: List = this.directives, ): GQLDirectiveDefinition = GQLDirectiveDefinition( sourceLocation = sourceLocation, description = description, @@ -713,6 +737,26 @@ class GQLDirectiveDefinition( arguments = arguments, repeatable = repeatable, locations = locations, + directives = directives, + ) + + @Deprecated("Kept for binary compatibility", level = DeprecationLevel.HIDDEN) + @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) + fun copy( + sourceLocation: SourceLocation? = this.sourceLocation, + description: String? = this.description, + name: String = this.name, + arguments: List = this.arguments, + repeatable: Boolean = this.repeatable, + locations: List = this.locations, + ): GQLDirectiveDefinition = GQLDirectiveDefinition( + sourceLocation = sourceLocation, + description = description, + name = name, + arguments = arguments, + repeatable = repeatable, + locations = locations, + directives = this.directives, ) override fun copyWithNewChildrenInternal(container: NodeContainer): GQLNode { @@ -1055,6 +1099,40 @@ class GQLUnionTypeExtension( } } +class GQLDirectiveExtension( + override val sourceLocation: SourceLocation? = null, + override val name: String, + override val directives: List, +) : GQLDefinition, GQLTypeSystemExtension, GQLNamed, GQLHasDirectives { + + override val children: List = directives + + override fun writeInternal(writer: SDLWriter) { + with(writer) { + write("extend directive @$name") + if (directives.isNotEmpty()) { + directives.join(writer, prefix = " ") + } + } + } + + fun copy( + sourceLocation: SourceLocation? = this.sourceLocation, + name: String = this.name, + directives: List = this.directives, + ): GQLDirectiveExtension = GQLDirectiveExtension( + sourceLocation = sourceLocation, + name = name, + directives = directives, + ) + + override fun copyWithNewChildrenInternal(container: NodeContainer): GQLNode { + return copy( + directives = container.take() + ) + } +} + class GQLEnumValueDefinition( override val sourceLocation: SourceLocation? = null, override val description: String?, @@ -2059,6 +2137,7 @@ enum class GQLDirectiveLocation { ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION, + DIRECTIVE_DEFINITION, } sealed interface GQLSchemaCoordinate diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt index 950d2310405..d10aa1541f2 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt @@ -3,6 +3,7 @@ package com.apollographql.apollo.ast.internal import com.apollographql.apollo.ast.GQLDefinition import com.apollographql.apollo.ast.GQLDirective import com.apollographql.apollo.ast.GQLDirectiveDefinition +import com.apollographql.apollo.ast.GQLDirectiveExtension import com.apollographql.apollo.ast.GQLEnumTypeDefinition import com.apollographql.apollo.ast.GQLEnumTypeExtension import com.apollographql.apollo.ast.GQLEnumValueDefinition @@ -62,6 +63,7 @@ internal class ExtensionsMerger(private val definitions: List, in is GQLInputObjectTypeExtension -> mergeNamedDefinition(GQLInputObjectTypeDefinition::class, newDefinitions, definition, "input") { mergeInputObject(it, definition) } is GQLEnumTypeExtension -> mergeNamedDefinition(GQLEnumTypeDefinition::class, newDefinitions, definition, "enum") { mergeEnum(it, definition) } is GQLUnionTypeExtension -> mergeNamedDefinition(GQLUnionTypeDefinition::class, newDefinitions, definition, "union") { mergeUnion(it, definition) } + is GQLDirectiveExtension -> mergeNamedDefinition(GQLDirectiveDefinition::class, newDefinitions, definition, "directive") { mergeDirective(it, definition) } } } @@ -175,6 +177,15 @@ private fun ExtensionsMerger.mergeScalar( ) } +private fun ExtensionsMerger.mergeDirective( + directiveDefinition: GQLDirectiveDefinition, + extension: GQLDirectiveExtension, +): GQLDirectiveDefinition = with(directiveDefinition) { + return copy( + directives = mergeDirectives(directives, extension.directives) + ) +} + private inline fun ExtensionsMerger.mergeNamedDefinition( @Suppress("UNUSED_PARAMETER") clazz: KClass, definitions: MutableList, diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/Parser.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/Parser.kt index 65427ab59ec..a055b6fca50 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/Parser.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/Parser.kt @@ -8,6 +8,7 @@ import com.apollographql.apollo.ast.GQLDirective import com.apollographql.apollo.ast.GQLDirectiveArgumentCoordinate import com.apollographql.apollo.ast.GQLDirectiveCoordinate import com.apollographql.apollo.ast.GQLDirectiveDefinition +import com.apollographql.apollo.ast.GQLDirectiveExtension import com.apollographql.apollo.ast.GQLDirectiveLocation import com.apollographql.apollo.ast.GQLDocument import com.apollographql.apollo.ast.GQLEnumTypeDefinition @@ -737,6 +738,7 @@ internal class Parser( expectToken() val name = parseName() val arguments = parseArgumentDefinitions() + val directives = parseDirectives(const = true) val repeatable = expectOptionalKeyword("repeatable") != null expectKeyword("on") val locations = parseDirectiveLocations() @@ -747,7 +749,25 @@ internal class Parser( name = name, arguments = arguments, repeatable = repeatable, - locations = locations + locations = locations, + directives = directives + ) + } + + private fun parseDirectiveExtension(): GQLDirectiveExtension { + val start = token + expectKeyword("extend") + expectKeyword("directive") + expectToken() + val name = parseName() + val directives = parseDirectives(const = true) + if (directives.isEmpty()) { + unexpected() + } + return GQLDirectiveExtension( + sourceLocation = sourceLocation(start), + name = name, + directives = directives ) } @@ -766,6 +786,7 @@ internal class Parser( "union" -> parseUnionTypeExtension() "enum" -> parseEnumTypeExtension() "input" -> parseInputObjectTypeExtension() + "directive" -> parseDirectiveExtension() else -> unexpected(t) } } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt index d1fe4ae62eb..9ad1a7c77c0 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt @@ -772,14 +772,15 @@ private fun ValidationScope.validateCatch(schemaDefinition: GQLSchemaDefinition) } private fun ValidationScope.validateDirectiveDefinitions() { - directiveDefinitions.values.forEach { - it.arguments.forEach { + directiveDefinitions.values.forEach { d -> + d.arguments.forEach { if (it.defaultValue != null) { validateAndCoerceValue(it.defaultValue, it.type, false, false) { issues.add(it.constContextError()) } } } + validateDirectivesInConstContext(d.directives, d) } } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ValidationCommon.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ValidationCommon.kt index b9495d587c7..120485b5dd3 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ValidationCommon.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ValidationCommon.kt @@ -89,6 +89,7 @@ internal class DefaultValidationScope( definitions.filterIsInstance().associateBy { it.name }, definitions.filterIsInstance().associateBy { it.name }, ) + constructor(schema: Schema) : this(schema.typeDefinitions, schema.directiveDefinitions) override val issues = issues ?: mutableListOf() @@ -147,6 +148,7 @@ internal fun ValidationScope.validateDirectives( } } + is GQLDirectiveDefinition -> GQLDirectiveLocation.DIRECTIVE_DEFINITION is GQLFragmentDefinition -> GQLDirectiveLocation.FRAGMENT_DEFINITION is GQLVariableDefinition -> GQLDirectiveLocation.VARIABLE_DEFINITION is GQLSchemaDefinition, is GQLSchemaExtension -> GQLDirectiveLocation.SCHEMA diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/introspection/introspection_reader.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/introspection/introspection_reader.kt index a337546f163..bfed8c0316d 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/introspection/introspection_reader.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/introspection/introspection_reader.kt @@ -32,6 +32,7 @@ import com.apollographql.apollo.ast.GQLUnionTypeDefinition import com.apollographql.apollo.ast.GQLValue import com.apollographql.apollo.ast.Schema import com.apollographql.apollo.ast.SourceLocation +import com.apollographql.apollo.ast.introspection.Optional.Absent import com.apollographql.apollo.ast.parseAsGQLValue import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -199,6 +200,8 @@ private class RDirective( val locations: Optional> = Optional.absent(), val args: Optional> = Optional.absent(), val isRepeatable: Boolean = false, + val isDeprecated: Optional = Optional.absent(), + val deprecationReason: Optional = Optional.absent(), ) @Serializable(with = OptionalSerializer::class) @@ -341,8 +344,8 @@ private class GQLDocumentBuilder(private val introspectionSchema: IntrospectionS private fun RField.toGQLFieldDefinition(): GQLFieldDefinition { val args = if (args is Optional.Absent) { - println("Apollo: $name.args is missing, double check your introspection query") - emptyList() + println("Apollo: $name.args is missing, double check your introspection query") + emptyList() } else { args.getOrThrow() } @@ -525,6 +528,11 @@ private class GQLDocumentBuilder(private val introspectionSchema: IntrospectionS arguments = args.map { it.toGQLInputValueDefinition() }, locations = locations.map { GQLDirectiveLocation.valueOf(it) }, repeatable = isRepeatable, + directives = if (deprecationReason == Absent) { + emptyList() + } else { + makeDirectives(deprecationReason.unwrapDeprecationReason(name)) + }, ) } } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/locations.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/locations.kt index 6c9cbf7ca84..6e91e04118f 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/locations.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/locations.kt @@ -23,6 +23,7 @@ internal fun GQLDefinition.removeLocation(): GQLDefinition = when(this) { is GQLScalarTypeDefinition -> this@removeLocation.removeLocation() is GQLUnionTypeDefinition -> this@removeLocation.removeLocation() is GQLUnionTypeExtension -> this@removeLocation.removeLocation() + is GQLDirectiveExtension -> this@removeLocation.removeLocation() } internal fun GQLEnumTypeDefinition.removeLocation() = copy( @@ -59,6 +60,10 @@ internal fun GQLUnionTypeExtension.removeLocation() = copy( directives = directives.map { it.removeLocation() }, memberTypes = memberTypes.map { it.removeLocation() } ) +internal fun GQLDirectiveExtension.removeLocation() = copy( + sourceLocation = null, + directives = directives.map { it.removeLocation() }, +) internal fun GQLFieldDefinition.removeLocation() = copy( sourceLocation = null, @@ -203,4 +208,4 @@ internal fun GQLValue.removeLocation() = when (this) { is GQLObjectValue -> copy(sourceLocation = null) is GQLStringValue -> copy(sourceLocation = null) is GQLVariableValue -> copy(sourceLocation = null) -} \ No newline at end of file +} diff --git a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt index 254f3794932..9ae700544a3 100644 --- a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt +++ b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt @@ -1,5 +1,8 @@ +@file:OptIn(ApolloExperimental::class) + package com.apollographql.apollo.graphql.ast.test +import com.apollographql.apollo.annotations.ApolloExperimental import com.apollographql.apollo.ast.ForeignSchema import com.apollographql.apollo.ast.internal.SchemaValidationOptions import com.apollographql.apollo.ast.internal.toSemanticSdl diff --git a/libraries/apollo-ast/test-fixtures/sdl/introspection.expected b/libraries/apollo-ast/test-fixtures/sdl/introspection.expected index 5bcccc75c5f..f12aafd0f44 100644 --- a/libraries/apollo-ast/test-fixtures/sdl/introspection.expected +++ b/libraries/apollo-ast/test-fixtures/sdl/introspection.expected @@ -5,22 +5,22 @@ type Query { """ Directs the executor to include this field or fragment only when the `if` argument is true """ -directive @include ("Included when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT """ Directs the executor to skip this field or fragment when the `if` argument is true. """ -directive @skip ("Skipped when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT """ Marks the field, argument, input field or enum value as deprecated """ -directive @deprecated ("The reason for the deprecation" reason: String = "No longer supported") on FIELD_DEFINITION|ARGUMENT_DEFINITION|ENUM_VALUE|INPUT_FIELD_DEFINITION +directive @deprecated("The reason for the deprecation" reason: String = "No longer supported") on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION """ Exposes a URL that specifies the behaviour of this scalar. """ -directive @specifiedBy ("The URL that specifies the behaviour of this scalar." url: String!) on SCALAR +directive @specifiedBy("The URL that specifies the behaviour of this scalar." url: String!) on SCALAR schema { query: Query diff --git a/libraries/apollo-ast/test-fixtures/sdl/simple.expected b/libraries/apollo-ast/test-fixtures/sdl/simple.expected index f61d6a2573e..8e1cc62c139 100644 --- a/libraries/apollo-ast/test-fixtures/sdl/simple.expected +++ b/libraries/apollo-ast/test-fixtures/sdl/simple.expected @@ -25,3 +25,5 @@ interface D implements A & B { b: String } + +directive @myDirective(arg1: String, arg2: Int = 42) @deprecated(reason: "Use newDirective instead") @myDirective(arg1: "test") on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT | DIRECTIVE_DEFINITION diff --git a/libraries/apollo-ast/test-fixtures/sdl/simple.graphqls b/libraries/apollo-ast/test-fixtures/sdl/simple.graphqls index 8222d5bad57..08b472f1e47 100644 --- a/libraries/apollo-ast/test-fixtures/sdl/simple.graphqls +++ b/libraries/apollo-ast/test-fixtures/sdl/simple.graphqls @@ -24,4 +24,6 @@ interface D implements A & B { a: String b: String -} \ No newline at end of file +} + +directive @myDirective(arg1: String, arg2: Int = 42) @deprecated(reason: "Use newDirective instead") @myDirective(arg1: "test") on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT | DIRECTIVE_DEFINITION diff --git a/libraries/apollo-compiler/src/test/validation/schema/unexpected-definitions.expected b/libraries/apollo-compiler/src/test/validation/schema/unexpected-definitions.expected index b8ba09d7749..599193c21ed 100644 --- a/libraries/apollo-compiler/src/test/validation/schema/unexpected-definitions.expected +++ b/libraries/apollo-compiler/src/test/validation/schema/unexpected-definitions.expected @@ -1,5 +1,5 @@ IncompatibleDefinition (1:1) -Unexpected 'catch' definition. Expecting 'directive @catch (levels: [Int!]! = [0], to: CatchTo! = RESULT) on FIELD'. +Unexpected 'catch' definition. Expecting 'directive @catch(levels: [Int!]! = [0], to: CatchTo! = RESULT) on FIELD'. ------------ IncompatibleDefinition (2:1) Unexpected 'CatchTo' definition. Expecting 'enum CatchTo { NULL RESULT THROW }'. diff --git a/libraries/apollo-tooling/src/test/fixtures/introspection-response-success-full.graphqls b/libraries/apollo-tooling/src/test/fixtures/introspection-response-success-full.graphqls index 3bf5e55aa4a..2cd1a2cf15d 100644 --- a/libraries/apollo-tooling/src/test/fixtures/introspection-response-success-full.graphqls +++ b/libraries/apollo-tooling/src/test/fixtures/introspection-response-success-full.graphqls @@ -466,22 +466,22 @@ enum __DirectiveLocation { """ Directs the executor to include this field or fragment only when the `if` argument is true. """ -directive @include ("Included when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT """ Directs the executor to skip this field or fragment when the `if` argument is true. """ -directive @skip ("Skipped when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT """ Marks an element of a GraphQL schema as no longer supported. """ -directive @deprecated ("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/)." reason: String! = "No longer supported") on FIELD_DEFINITION|ARGUMENT_DEFINITION|INPUT_FIELD_DEFINITION|ENUM_VALUE +directive @deprecated("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/)." reason: String! = "No longer supported") on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE """ Exposes a URL that specifies the behavior of this scalar. """ -directive @specifiedBy ("The URL that specifies the behavior of this scalar." url: String!) on SCALAR +directive @specifiedBy("The URL that specifies the behavior of this scalar." url: String!) on SCALAR """ Indicates exactly one field must be supplied and this field must not be `null`. diff --git a/tests/sample-server/graphql/schema.graphqls b/tests/sample-server/graphql/schema.graphqls index fc4f3b3109a..1c7ec13d371 100644 --- a/tests/sample-server/graphql/schema.graphqls +++ b/tests/sample-server/graphql/schema.graphqls @@ -190,12 +190,12 @@ enum __DirectiveLocation { INPUT_FIELD_DEFINITION } -directive @skip (if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @include (if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @deprecated (reason: String! = "No longer supported") on FIELD_DEFINITION|ARGUMENT_DEFINITION|INPUT_FIELD_DEFINITION|ENUM_VALUE +directive @deprecated(reason: String! = "No longer supported") on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE -directive @defer (label: String, if: Boolean! = true) on FRAGMENT_SPREAD|INLINE_FRAGMENT +directive @defer(label: String, if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @specifiedBy (url: String!) on SCALAR +directive @specifiedBy(url: String!) on SCALAR From 55cbd1e9f6fb9ab3932f62594583ef6a94b15d2d Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 5 Dec 2025 18:20:50 +0100 Subject: [PATCH 2/3] Add support for deprecated directives in introspection --- .../apollo/tooling/GraphQLFeature.kt | 18 +- .../apollo/tooling/SchemaHelper.kt | 159 ++++++++++-------- .../main/resources/base-introspection.graphql | 4 +- ...st-september2025-deprecatedDirectives.json | 1 + ...ospection-request-september2025-oneOf.json | 1 - .../introspection-request-september2025.json | 2 +- ...e-september2025-deprecatedDirectives.json} | 14 +- ...-introspection-response-september2025.json | 4 + .../apollo/tooling/SchemaDownloaderTests.kt | 14 +- 9 files changed, 132 insertions(+), 85 deletions(-) create mode 100644 libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-deprecatedDirectives.json delete mode 100644 libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-oneOf.json rename libraries/apollo-tooling/src/test/fixtures/{pre-introspection-response-september2025-oneOf.json => pre-introspection-response-september2025-deprecatedDirectives.json} (93%) diff --git a/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/GraphQLFeature.kt b/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/GraphQLFeature.kt index 4042f1be631..6d8f9a9d7ba 100644 --- a/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/GraphQLFeature.kt +++ b/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/GraphQLFeature.kt @@ -48,7 +48,17 @@ enum class GraphQLFeature { * * Introspection: `__Type.isOneOf`. */ - OneOf + OneOf, + + /** + * Deprecated directives, introduced in [Add support for directives on directives](https://github.com/graphql/graphql-spec/pull/907/). + * + * Introspection: + * - `__Directive.isDeprecated` + * - `__Directive.deprecationReason` + * - `__Schema.directives`'s `includeDeprecated` argument + */ + DeprecatedDirectives, } internal fun PreIntrospectionQuery.Data.getFeatures(): Set { @@ -77,6 +87,12 @@ internal fun PreIntrospectionQuery.Data.getFeatures(): Set { if (directiveFields.any { it.name == "isRepeatable" }) { add(RepeatableDirectives) } + if (directiveFields.any { it.name == "isDeprecated" } && + directiveFields.any { it.name == "deprecationReason" } && + schema?.typeFields?.fields?.firstOrNull { it.name == "directives" }?.args?.any { it.name == "includeDeprecated" } == true + ) { + add(GraphQLFeature.DeprecatedDirectives) + } val directiveArgsIncludeDeprecated = directiveFields.firstOrNull { it.name == "args" }?.let { args -> args.args.any { it.name == "includeDeprecated" } } == true diff --git a/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/SchemaHelper.kt b/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/SchemaHelper.kt index a20595ea62a..986fa0c5258 100644 --- a/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/SchemaHelper.kt +++ b/libraries/apollo-tooling/src/main/kotlin/com/apollographql/apollo/tooling/SchemaHelper.kt @@ -108,82 +108,94 @@ internal object SchemaHelper { } internal fun List.reworkIntrospectionQuery(features: Set) = - mapIf<_, GQLOperationDefinition>({ it.name == "IntrospectionQuery" }) { - it.copy( - selections = it.selections - // Add __schema { description } - .mapIf(SchemaDescription in features) { schemaField -> - schemaField as GQLField - schemaField.copy( - selections = schemaField.selections + createField("description") - ) - } - // Add __schema { directives { isRepeatable } } - .mapIf(RepeatableDirectives in features) { schemaField -> - schemaField as GQLField - schemaField.copy( - selections = schemaField.selections.mapIf<_, GQLField>({ it.name == "directives" }) { directivesField -> - directivesField.copy(selections = directivesField.selections + createField("isRepeatable")) - } - ) - } - // Replace __schema { directives { args { ... } } } by __schema { directives { args(includeDeprecated: true) { ... } } } - .mapIf(DeprecatedInputValues in features) { schemaField -> - schemaField as GQLField - schemaField.copy( - selections = schemaField.selections.mapIf<_, GQLField>({ it.name == "directives" }) { directivesField -> - directivesField.copy( - selections = directivesField.selections.mapIf<_, GQLField>({ it.name == "args" }) { argsField -> - argsField.copy(arguments = listOf(GQLArgument(name = "includeDeprecated", value = GQLBooleanValue(value = true)))) - } - ) - } - ) - } - ) - } + mapIf<_, GQLOperationDefinition>({ it.name == "IntrospectionQuery" }) { + it.copy( + selections = it.selections + // Add __schema { description } + .mapIf(SchemaDescription in features) { schemaField -> + schemaField as GQLField + schemaField.copy( + selections = schemaField.selections + createField("description") + ) + } + // Add __schema { directives { isRepeatable } } + .mapIf(RepeatableDirectives in features) { schemaField -> + schemaField as GQLField + schemaField.copy( + selections = schemaField.selections.mapIf<_, GQLField>({ it.name == "directives" }) { directivesField -> + directivesField.copy(selections = directivesField.selections + createField("isRepeatable")) + } + ) + } + // Add __schema { directives(includeDeprecated: true) { isDeprecated deprecationReason } } + .mapIf(DeprecatedDirectives in features) { schemaField -> + schemaField as GQLField + schemaField.copy( + selections = schemaField.selections.mapIf<_, GQLField>({ it.name == "directives" }) { directivesField -> + directivesField.copy( + arguments = listOf(GQLArgument(name = "includeDeprecated", value = GQLBooleanValue(value = true))), + selections = directivesField.selections + createField("isDeprecated") + createField("deprecationReason") + ) + } + ) + } + // Replace __schema { directives { args { ... } } } by __schema { directives { args(includeDeprecated: true) { ... } } } + .mapIf(DeprecatedInputValues in features) { schemaField -> + schemaField as GQLField + schemaField.copy( + selections = schemaField.selections.mapIf<_, GQLField>({ it.name == "directives" }) { directivesField -> + directivesField.copy( + selections = directivesField.selections.mapIf<_, GQLField>({ it.name == "args" }) { argsField -> + argsField.copy(arguments = listOf(GQLArgument(name = "includeDeprecated", value = GQLBooleanValue(value = true)))) + } + ) + } + ) + } + ) + } internal fun List.reworkFullTypeFragment(features: Set) = - mapIf<_, GQLFragmentDefinition>({ it.name == "FullType" }) { - it.copy( - selections = it.selections - // Add specifiedByUrl - .letIf(SpecifiedBy in features) { fields -> - fields + createField("specifiedByURL") - } - // Add isOneOf - .letIf(OneOf in features) { fields -> - fields + createField("isOneOf") - } - // Replace inputFields { ... } by inputFields(includeDeprecated: true) { ... } - .mapIf<_, GQLField>({ DeprecatedInputValues in features && it.name == "inputFields" }) { inputFieldsField -> - inputFieldsField.copy(arguments = listOf(GQLArgument(name = "includeDeprecated", value = GQLBooleanValue(value = true)))) - } - // Replace fields { args { ... } } by fields { args(includeDeprecated: true) { ... } } - .mapIf<_, GQLField>({ DeprecatedInputValues in features && it.name == "fields" }) { fieldsField -> - fieldsField.copy( - selections = fieldsField.selections.mapIf<_, GQLField>({ it.name == "args" }) { argsField -> - argsField.copy(arguments = listOf(GQLArgument(name = "includeDeprecated", value = GQLBooleanValue(value = true)))) - } - ) - } - ) - } + mapIf<_, GQLFragmentDefinition>({ it.name == "FullType" }) { + it.copy( + selections = it.selections + // Add specifiedByUrl + .letIf(SpecifiedBy in features) { fields -> + fields + createField("specifiedByURL") + } + // Add isOneOf + .letIf(OneOf in features) { fields -> + fields + createField("isOneOf") + } + // Replace inputFields { ... } by inputFields(includeDeprecated: true) { ... } + .mapIf<_, GQLField>({ DeprecatedInputValues in features && it.name == "inputFields" }) { inputFieldsField -> + inputFieldsField.copy(arguments = listOf(GQLArgument(name = "includeDeprecated", value = GQLBooleanValue(value = true)))) + } + // Replace fields { args { ... } } by fields { args(includeDeprecated: true) { ... } } + .mapIf<_, GQLField>({ DeprecatedInputValues in features && it.name == "fields" }) { fieldsField -> + fieldsField.copy( + selections = fieldsField.selections.mapIf<_, GQLField>({ it.name == "args" }) { argsField -> + argsField.copy(arguments = listOf(GQLArgument(name = "includeDeprecated", value = GQLBooleanValue(value = true)))) + } + ) + } + ) + } internal fun List.reworkInputValueFragment(features: Set) = - mapIf<_, GQLFragmentDefinition>({ it.name == "InputValue" }) { - it.copy( - selections = it.selections - // Add isDeprecated - .letIf(DeprecatedInputValues in features) { fields -> - fields + createField("isDeprecated") - } - // Add deprecationReason - .letIf(DeprecatedInputValues in features) { fields -> - fields + createField("deprecationReason") - } - ) - } + mapIf<_, GQLFragmentDefinition>({ it.name == "InputValue" }) { + it.copy( + selections = it.selections + // Add isDeprecated + .letIf(DeprecatedInputValues in features) { fields -> + fields + createField("isDeprecated") + } + // Add deprecationReason + .letIf(DeprecatedInputValues in features) { fields -> + fields + createField("deprecationReason") + } + ) + } private inline fun T.letIf(condition: Boolean, block: (T) -> T): T = if (condition) block(this) else this @@ -194,7 +206,8 @@ internal object SchemaHelper { block: (E) -> E, ): List = map { if (it is E && condition(it)) block(it) else it } - private fun createField(name: String) = GQLField(alias = null, name = name, arguments = emptyList(), directives = emptyList(), selections = emptyList()) + private fun createField(name: String) = + GQLField(alias = null, name = name, arguments = emptyList(), directives = emptyList(), selections = emptyList()) private fun OkHttpClient.Builder.applyInsecureTrustManager() = apply { val insecureTrustManager = InsecureTrustManager() diff --git a/libraries/apollo-tooling/src/main/resources/base-introspection.graphql b/libraries/apollo-tooling/src/main/resources/base-introspection.graphql index eb967db7460..817f1520a3e 100644 --- a/libraries/apollo-tooling/src/main/resources/base-introspection.graphql +++ b/libraries/apollo-tooling/src/main/resources/base-introspection.graphql @@ -12,7 +12,7 @@ query IntrospectionQuery { types { ...FullType } - directives { + directives { # directives(includeDeprecated: true) - introduced in https://github.com/graphql/graphql-spec/pull/907 name description locations @@ -20,6 +20,8 @@ query IntrospectionQuery { ...InputValue } # isRepeatable - introduced in https://spec.graphql.org/October2021/ + # isDeprecated - introduced in https://github.com/graphql/graphql-spec/pull/907 + # deprecationReason - introduced in https://github.com/graphql/graphql-spec/pull/907 } } } diff --git a/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-deprecatedDirectives.json b/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-deprecatedDirectives.json new file mode 100644 index 00000000000..50945857ef2 --- /dev/null +++ b/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-deprecatedDirectives.json @@ -0,0 +1 @@ +{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives(includeDeprecated: true) {\n name\n description\n locations\n args(includeDeprecated: true) {\n ...InputValue\n }\n isRepeatable\n isDeprecated\n deprecationReason\n }\n description\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args(includeDeprecated: true) {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields(includeDeprecated: true) {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n specifiedByURL\n isOneOf\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n isDeprecated\n deprecationReason\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"} \ No newline at end of file diff --git a/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-oneOf.json b/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-oneOf.json deleted file mode 100644 index ad9b10c5b14..00000000000 --- a/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025-oneOf.json +++ /dev/null @@ -1 +0,0 @@ -{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args(includeDeprecated: true) {\n ...InputValue\n }\n isRepeatable\n }\n description\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args(includeDeprecated: true) {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields(includeDeprecated: true) {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n specifiedByURL\n isOneOf\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n isDeprecated\n deprecationReason\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"} \ No newline at end of file diff --git a/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025.json b/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025.json index 76fddbac134..ad9b10c5b14 100644 --- a/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025.json +++ b/libraries/apollo-tooling/src/test/fixtures/introspection-request-september2025.json @@ -1 +1 @@ -{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args(includeDeprecated: true) {\n ...InputValue\n }\n isRepeatable\n }\n description\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args(includeDeprecated: true) {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields(includeDeprecated: true) {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n specifiedByURL\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n isDeprecated\n deprecationReason\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"} \ No newline at end of file +{"operationName":"IntrospectionQuery","query":"query IntrospectionQuery {\n __schema {\n queryType {\n name\n }\n mutationType {\n name\n }\n subscriptionType {\n name\n }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args(includeDeprecated: true) {\n ...InputValue\n }\n isRepeatable\n }\n description\n }\n}\n\nfragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args(includeDeprecated: true) {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields(includeDeprecated: true) {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n specifiedByURL\n isOneOf\n}\n\nfragment InputValue on __InputValue {\n name\n description\n type {\n ...TypeRef\n }\n defaultValue\n isDeprecated\n deprecationReason\n}\n\nfragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n}\n"} \ No newline at end of file diff --git a/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-oneOf.json b/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-deprecatedDirectives.json similarity index 93% rename from libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-oneOf.json rename to libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-deprecatedDirectives.json index cbf25107aed..ffaac6d1647 100644 --- a/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-oneOf.json +++ b/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-deprecatedDirectives.json @@ -25,7 +25,11 @@ }, { "name": "directives", - "args": [] + "args": [ + { + "name": "includeDeprecated" + } + ] }, { "name": "description", @@ -122,6 +126,14 @@ { "name": "isRepeatable", "args": [] + }, + { + "name": "isDeprecated", + "args": [] + }, + { + "name": "deprecationReason", + "args": [] } ] }, diff --git a/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025.json b/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025.json index 83c73792b29..cbf25107aed 100644 --- a/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025.json +++ b/libraries/apollo-tooling/src/test/fixtures/pre-introspection-response-september2025.json @@ -88,6 +88,10 @@ { "name": "specifiedByURL", "args": [] + }, + { + "name": "isOneOf", + "args": [] } ] }, diff --git a/libraries/apollo-tooling/src/test/kotlin/com/apollographql/apollo/tooling/SchemaDownloaderTests.kt b/libraries/apollo-tooling/src/test/kotlin/com/apollographql/apollo/tooling/SchemaDownloaderTests.kt index 1a7c36b172e..364da3b1e3c 100644 --- a/libraries/apollo-tooling/src/test/kotlin/com/apollographql/apollo/tooling/SchemaDownloaderTests.kt +++ b/libraries/apollo-tooling/src/test/kotlin/com/apollographql/apollo/tooling/SchemaDownloaderTests.kt @@ -20,10 +20,10 @@ private val preIntrospectionResponseSeptember2025 = pathToUtf8("apollo-tooling/src/test/fixtures/pre-introspection-response-september2025.json") private val introspectionRequestSeptember2025 = pathToUtf8("apollo-tooling/src/test/fixtures/introspection-request-september2025.json") -private val preIntrospectionResponseSeptember2025OneOf = - pathToUtf8("apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-oneOf.json") -private val introspectionRequestSeptember2025OneOf = - pathToUtf8("apollo-tooling/src/test/fixtures/introspection-request-september2025-oneOf.json") +private val preIntrospectionResponseSeptember2025DeprecatedDirectives = + pathToUtf8("apollo-tooling/src/test/fixtures/pre-introspection-response-september2025-deprecatedDirectives.json") +private val introspectionRequestSeptember2025DeprecatedDirectives = + pathToUtf8("apollo-tooling/src/test/fixtures/introspection-request-september2025-deprecatedDirectives.json") private val introspectionRequestFailSafe = pathToUtf8("apollo-tooling/src/test/fixtures/introspection-request-failSafe.json") @@ -127,9 +127,9 @@ class SchemaDownloaderTests { } @Test - fun `schema is downloaded correctly when server supports September 2025 spec including oneOf`() = + fun `schema is downloaded correctly when server supports September 2025 spec with deprecated directives`() = runTest(before = { setUp() }, after = { tearDown() }) { - mockServer.enqueueString(preIntrospectionResponseSeptember2025OneOf) + mockServer.enqueueString(preIntrospectionResponseSeptember2025DeprecatedDirectives) mockServer.enqueueString(introspectionResponseSuccess) SchemaDownloader.download( @@ -142,7 +142,7 @@ class SchemaDownloaderTests { mockServer.takeRequest() val introspectionRequest = mockServer.takeRequest().body.utf8() - assertEquals(introspectionRequestSeptember2025OneOf, introspectionRequest) + assertEquals(introspectionRequestSeptember2025DeprecatedDirectives, introspectionRequest) assertEquals(introspectionResponseSuccess, tempFileJson.readText()) } From b0c6f44befa629762c5e4fe09dc6b209161765d2 Mon Sep 17 00:00:00 2001 From: BoD Date: Mon, 8 Dec 2025 09:33:03 +0100 Subject: [PATCH 3/3] Update API dump --- libraries/apollo-tooling/api/apollo-tooling.api | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/apollo-tooling/api/apollo-tooling.api b/libraries/apollo-tooling/api/apollo-tooling.api index f0a15bf4999..091f16fa035 100644 --- a/libraries/apollo-tooling/api/apollo-tooling.api +++ b/libraries/apollo-tooling/api/apollo-tooling.api @@ -37,6 +37,7 @@ public final class com/apollographql/apollo/tooling/GraphNotFound : com/apollogr } public final class com/apollographql/apollo/tooling/GraphQLFeature : java/lang/Enum { + public static final field DeprecatedDirectives Lcom/apollographql/apollo/tooling/GraphQLFeature; public static final field DeprecatedInputValues Lcom/apollographql/apollo/tooling/GraphQLFeature; public static final field OneOf Lcom/apollographql/apollo/tooling/GraphQLFeature; public static final field RepeatableDirectives Lcom/apollographql/apollo/tooling/GraphQLFeature;