From 0dd03675dc7c971abfad86cafaf2da353778529f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory?= Date: Fri, 25 Jul 2025 17:00:15 +0200 Subject: [PATCH 1/2] Add missing `__Directive.args(includeDeprecated:)` Adds the missing includeDeprecated argument to `__Directive.args` field to match GraphQL.js implementation (graphql/graphql-js#3273). The argument allows filtering deprecated directive arguments from introspection results when set to false (default behavior). This notably fixes schema generation with `graphql-codegen` when using the "inputValueDeprecation" option (see dotansimha/graphql-code-generator#9659). Includes tests for deprecated argument filtering that I didn't find in GraphQL.js. --- src/Type/Introspection.php | 29 +++++++-- tests/Type/IntrospectionTest.php | 98 ++++++++++++++++++++++++++++++- tests/Utils/SchemaPrinterTest.php | 2 +- 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index b1bd06a55..df3d75e6b 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -110,7 +110,7 @@ public static function getIntrospectionQuery(array $options = []): string directives { name {$descriptions} - args { + args(includeDeprecated: true) { ...InputValue } {$directiveIsRepeatable} @@ -362,7 +362,7 @@ public static function _type(): ObjectType if ($type instanceof ObjectType || $type instanceof InterfaceType) { $fields = $type->getVisibleFields(); - if (! ($args['includeDeprecated'] ?? false)) { + if (! $args['includeDeprecated']) { return array_filter( $fields, static fn (FieldDefinition $field): bool => ! $field->isDeprecated() @@ -399,7 +399,7 @@ public static function _type(): ObjectType if ($type instanceof EnumType) { $values = $type->getValues(); - if (! ($args['includeDeprecated'] ?? false)) { + if (! $args['includeDeprecated']) { return array_filter( $values, static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated() @@ -424,7 +424,7 @@ public static function _type(): ObjectType if ($type instanceof InputObjectType) { $fields = $type->getFields(); - if (! ($args['includeDeprecated'] ?? false)) { + if (! $args['includeDeprecated']) { return array_filter( $fields, static fn (InputObjectField $field): bool => ! $field->isDeprecated(), @@ -523,7 +523,7 @@ public static function _field(): ObjectType 'resolve' => static function (FieldDefinition $field, $args): array { $values = $field->args; - if (! ($args['includeDeprecated'] ?? false)) { + if (! $args['includeDeprecated']) { return array_filter( $values, static fn (Argument $value): bool => ! $value->isDeprecated(), @@ -667,7 +667,24 @@ public static function _directive(): ObjectType ], 'args' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), - 'resolve' => static fn (Directive $directive): array => $directive->args, + 'args' => [ + 'includeDeprecated' => [ + 'type' => Type::boolean(), + 'defaultValue' => false, + ], + ], + 'resolve' => static function (Directive $directive, $args): array { + $values = $directive->args; + + if (! $args['includeDeprecated']) { + return array_filter( + $values, + static fn (Argument $value): bool => ! $value->isDeprecated(), + ); + } + + return $values; + }, ], ], ]); diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index 5fe8b1d38..d036be4b8 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -4,8 +4,10 @@ use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use GraphQL\GraphQL; +use GraphQL\Language\DirectiveLocation; use GraphQL\Language\SourceLocation; use GraphQL\Tests\ErrorHelper; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; @@ -775,7 +777,19 @@ public function testExecutesAnIntrospectionQuery(): void ], [ 'name' => 'args', - 'args' => [], + 'args' => [ + 0 => [ + 'name' => 'includeDeprecated', + 'type' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], + 'defaultValue' => 'false', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + ], 'type' => [ 'kind' => 'NON_NULL', 'name' => null, @@ -1872,4 +1886,86 @@ public function testIsOneOf(): void $introspection = Introspection::fromSchema($schema); self::assertTrue($introspection['__schema']['types'][1]['isOneOf']); } + + public function testRespectsTheIncludeDeprecatedParameterForDirectiveArgs(): void + { + $query = new ObjectType([ + 'name' => 'QueryRoot', + 'fields' => [ + [ + 'name' => 'test', + 'type' => Type::int(), + ], + ], + ]); + + $directive = new Directive([ + 'name' => 'TestDirective', + 'args' => [ + 'nonDeprecated' => [ + 'type' => Type::string(), + ], + 'deprecated' => [ + 'type' => Type::string(), + 'deprecationReason' => 'Removed in 1.0', + ], + ], + 'locations' => [DirectiveLocation::FIELD], + ]); + + $schema = new Schema([ + 'query' => $query, + 'directives' => [$directive], + ]); + + $request = ' + { + __schema { + directives { + trueArgs: args(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + falseArgs: args(includeDeprecated: false) { + name + } + omittedArgs: args { + name + } + } + } + } + '; + + $expected = [ + [ + 'trueArgs' => [ + [ + 'name' => 'nonDeprecated', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'deprecated', + 'isDeprecated' => true, + 'deprecationReason' => 'Removed in 1.0', + ], + ], + 'falseArgs' => [ + [ + 'name' => 'nonDeprecated', + ], + ], + 'omittedArgs' => [ + [ + 'name' => 'nonDeprecated', + ], + ], + ], + ]; + + $result = GraphQL::executeQuery($schema, $request)->toArray(); + self::assertSame($expected, $result['data']['__schema']['directives'] ?? null); + } } diff --git a/tests/Utils/SchemaPrinterTest.php b/tests/Utils/SchemaPrinterTest.php index b72500aa4..51f00a4fb 100644 --- a/tests/Utils/SchemaPrinterTest.php +++ b/tests/Utils/SchemaPrinterTest.php @@ -1160,7 +1160,7 @@ enum __TypeKind { description: String isRepeatable: Boolean! locations: [__DirectiveLocation!]! - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! } "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies." From 851ddb55d722035904191705b6d62ba56950680c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory?= Date: Mon, 28 Jul 2025 09:46:48 +0200 Subject: [PATCH 2/2] Make `includeDeprecated` non-null See graphql/graphql-spec#1142 and graphql/graphql-js#4354 --- src/Type/Introspection.php | 10 +++---- tests/Type/IntrospectionTest.php | 50 +++++++++++++++++++++---------- tests/Utils/SchemaPrinterTest.php | 10 +++---- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index df3d75e6b..f9c525910 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -354,7 +354,7 @@ public static function _type(): ObjectType 'type' => Type::listOf(Type::nonNull(self::_field())), 'args' => [ 'includeDeprecated' => [ - 'type' => Type::boolean(), + 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], @@ -391,7 +391,7 @@ public static function _type(): ObjectType 'type' => Type::listOf(Type::nonNull(self::_enumValue())), 'args' => [ 'includeDeprecated' => [ - 'type' => Type::boolean(), + 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], @@ -416,7 +416,7 @@ public static function _type(): ObjectType 'type' => Type::listOf(Type::nonNull(self::_inputValue())), 'args' => [ 'includeDeprecated' => [ - 'type' => Type::boolean(), + 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], @@ -516,7 +516,7 @@ public static function _field(): ObjectType 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), 'args' => [ 'includeDeprecated' => [ - 'type' => Type::boolean(), + 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], @@ -669,7 +669,7 @@ public static function _directive(): ObjectType 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), 'args' => [ 'includeDeprecated' => [ - 'type' => Type::boolean(), + 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index d036be4b8..c9ed89d1d 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -237,9 +237,13 @@ public function testExecutesAnIntrospectionQuery(): void 0 => [ 'name' => 'includeDeprecated', 'type' => [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => null, + 'kind' => 'NON_NULL', + 'name' => null, + 'ofType' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], ], 'defaultValue' => 'false', 'isDeprecated' => false, @@ -306,9 +310,13 @@ public function testExecutesAnIntrospectionQuery(): void 0 => [ 'name' => 'includeDeprecated', 'type' => [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => null, + 'kind' => 'NON_NULL', + 'name' => null, + 'ofType' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], ], 'defaultValue' => 'false', 'isDeprecated' => false, @@ -337,9 +345,13 @@ public function testExecutesAnIntrospectionQuery(): void 0 => [ 'name' => 'includeDeprecated', 'type' => [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => null, + 'kind' => 'NON_NULL', + 'name' => null, + 'ofType' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], ], 'defaultValue' => 'false', 'isDeprecated' => false, @@ -478,9 +490,13 @@ public function testExecutesAnIntrospectionQuery(): void 0 => [ 'name' => 'includeDeprecated', 'type' => [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => null, + 'kind' => 'NON_NULL', + 'name' => null, + 'ofType' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], ], 'defaultValue' => 'false', 'isDeprecated' => false, @@ -781,9 +797,13 @@ public function testExecutesAnIntrospectionQuery(): void 0 => [ 'name' => 'includeDeprecated', 'type' => [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - 'ofType' => null, + 'kind' => 'NON_NULL', + 'name' => null, + 'ofType' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], ], 'defaultValue' => 'false', 'isDeprecated' => false, diff --git a/tests/Utils/SchemaPrinterTest.php b/tests/Utils/SchemaPrinterTest.php index 51f00a4fb..37f4de3c3 100644 --- a/tests/Utils/SchemaPrinterTest.php +++ b/tests/Utils/SchemaPrinterTest.php @@ -1083,11 +1083,11 @@ public function testPrintIntrospectionSchema(): void kind: __TypeKind! name: String description: String - fields(includeDeprecated: Boolean = false): [__Field!] + fields(includeDeprecated: Boolean! = false): [__Field!] interfaces: [__Type!] possibleTypes: [__Type!] - enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields(includeDeprecated: Boolean = false): [__InputValue!] + enumValues(includeDeprecated: Boolean! = false): [__EnumValue!] + inputFields(includeDeprecated: Boolean! = false): [__InputValue!] ofType: __Type isOneOf: Boolean } @@ -1123,7 +1123,7 @@ enum __TypeKind { type __Field { name: String! description: String - args(includeDeprecated: Boolean = false): [__InputValue!]! + args(includeDeprecated: Boolean! = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -1160,7 +1160,7 @@ enum __TypeKind { description: String isRepeatable: Boolean! locations: [__DirectiveLocation!]! - args(includeDeprecated: Boolean = false): [__InputValue!]! + args(includeDeprecated: Boolean! = false): [__InputValue!]! } "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies."