From 9e7e39440128c377237fcec373273b6463142d5b Mon Sep 17 00:00:00 2001 From: TaProhm Date: Wed, 10 May 2023 02:26:12 +0300 Subject: [PATCH 1/2] Allow deprecating input fields and arguments --- phpstan-baseline.neon | 2 +- src/Type/Definition/Argument.php | 14 ++ src/Type/Definition/Directive.php | 2 + src/Type/Definition/InputObjectField.php | 14 ++ src/Type/Introspection.php | 67 +++++- src/Utils/ASTDefinitionBuilder.php | 3 +- src/Utils/SchemaExtender.php | 2 + src/Utils/SchemaPrinter.php | 8 +- tests/Type/DefinitionTest.php | 20 +- tests/Type/DirectiveTest.php | 55 +++++ tests/Type/IntrospectionTest.php | 290 ++++++++++++++++++++++- tests/Type/ValidationTest.php | 43 ++++ tests/Utils/BuildSchemaTest.php | 10 +- tests/Utils/SchemaExtenderTest.php | 21 +- tests/Utils/SchemaPrinterTest.php | 9 +- tests/Validator/QueryComplexityTest.php | 2 +- 16 files changed, 534 insertions(+), 28 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 377cba074..2479e37d4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -26,7 +26,7 @@ parameters: path: src/Language/Visitor.php - - message: "#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\Argument constructor expects array\\{name\\: string, type\\: \\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|\\(GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\), defaultValue\\?\\: mixed, description\\?\\: string\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InputValueDefinitionNode\\|null\\}, non\\-empty\\-array given\\.$#" + message: "#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\Argument constructor expects array\\{name\\: string, type\\: \\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|\\(GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\), defaultValue\\?\\: mixed, description\\?\\: string\\|null, deprecationReason\\?\\: string\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InputValueDefinitionNode\\|null\\}, non\\-empty\\-array given\\.$#" count: 1 path: src/Type/Definition/Argument.php diff --git a/src/Type/Definition/Argument.php b/src/Type/Definition/Argument.php index a1da4b834..ce874b410 100644 --- a/src/Type/Definition/Argument.php +++ b/src/Type/Definition/Argument.php @@ -14,6 +14,7 @@ * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, + * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } * @phpstan-type ArgumentConfig array{ @@ -21,6 +22,7 @@ * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, + * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } * @phpstan-type ArgumentListConfig iterable|iterable @@ -34,6 +36,8 @@ class Argument public ?string $description; + public ?string $deprecationReason; + /** @var Type&InputType */ private Type $type; @@ -48,6 +52,7 @@ public function __construct(array $config) $this->name = $config['name']; $this->defaultValue = $config['defaultValue'] ?? null; $this->description = $config['description'] ?? null; + $this->deprecationReason = $config['deprecationReason'] ?? null; // Do nothing for type, it is lazy loaded in getType() $this->astNode = $config['astNode'] ?? null; @@ -95,6 +100,11 @@ public function isRequired(): bool && ! $this->defaultValueExists(); } + public function isDeprecated(): bool + { + return (bool) $this->deprecationReason; + } + /** * @param Type&NamedType $parentType * @@ -113,5 +123,9 @@ public function assertValid(FieldDefinition $parentField, Type $parentType): voi $notInputType = Utils::printSafe($this->type); throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}): argument type must be Input Type but got: {$notInputType}"); } + + if ($this->isRequired() && $this->isDeprecated()) { + throw new InvariantViolation("Required argument {$parentType->name}.{$parentField->name}({$this->name}:) cannot be deprecated."); + } } } diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index 00ce3ab8a..0a6db42e5 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -127,6 +127,8 @@ public static function getInternalDirectives(): array 'locations' => [ DirectiveLocation::FIELD_DEFINITION, DirectiveLocation::ENUM_VALUE, + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::INPUT_FIELD_DEFINITION, ], 'args' => [ self::REASON_ARGUMENT_NAME => [ diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index 99f6bfc45..604ab308a 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -14,6 +14,7 @@ * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, + * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } * @phpstan-type UnnamedInputObjectFieldConfig array{ @@ -21,6 +22,7 @@ * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, + * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } */ @@ -33,6 +35,8 @@ class InputObjectField public ?string $description; + public ?string $deprecationReason; + /** @var Type&InputType */ private Type $type; @@ -47,6 +51,7 @@ public function __construct(array $config) $this->name = $config['name']; $this->defaultValue = $config['defaultValue'] ?? null; $this->description = $config['description'] ?? null; + $this->deprecationReason = $config['deprecationReason'] ?? null; // Do nothing for type, it is lazy loaded in getType() $this->astNode = $config['astNode'] ?? null; @@ -74,6 +79,11 @@ public function isRequired(): bool && ! $this->defaultValueExists(); } + public function isDeprecated(): bool + { + return (bool) $this->deprecationReason; + } + /** * @param Type&NamedType $parentType * @@ -97,5 +107,9 @@ public function assertValid(Type $parentType): void if (\array_key_exists('resolve', $this->config)) { throw new InvariantViolation("{$parentType->name}.{$this->name} field has a resolve property, but Input Types cannot define resolvers."); } + + if ($this->isRequired() && $this->isDeprecated()) { + throw new InvariantViolation("Required input field {$parentType->name}.{$this->name} cannot be deprecated."); + } } } diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index c3d4a55b8..dddcfa93a 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -96,7 +96,7 @@ public static function getIntrospectionQuery(array $options = []): string fields(includeDeprecated: true) { name {$descriptions} - args { + args(includeDeprecated: true) { ...InputValue } type { @@ -105,7 +105,7 @@ public static function getIntrospectionQuery(array $options = []): string isDeprecated deprecationReason } - inputFields { + inputFields(includeDeprecated: true) { ...InputValue } interfaces { @@ -127,6 +127,8 @@ enumValues(includeDeprecated: true) { {$descriptions} type { ...TypeRef } defaultValue + isDeprecated + deprecationReason } fragment TypeRef on __Type { @@ -392,9 +394,31 @@ static function (EnumValueDefinition $value): bool { ], 'inputFields' => [ 'type' => Type::listOf(Type::nonNull(self::_inputValue())), - 'resolve' => static fn ($type): ?array => $type instanceof InputObjectType - ? $type->getFields() - : null, + 'args' => [ + 'includeDeprecated' => [ + 'type' => Type::boolean(), + 'defaultValue' => false, + ], + ], + 'resolve' => static function ($type, $args): ?array { + if ($type instanceof InputObjectType) { + $fields = $type->getFields(); + + if (! ($args['includeDeprecated'] ?? false)) { + return \array_filter( + $fields, + static fn (InputObjectField $field): bool => + $field->deprecationReason === null + || $field->deprecationReason === '', + + ); + } + + return $fields; + } + + return null; + }, ], 'ofType' => [ 'type' => self::_type(), @@ -469,7 +493,27 @@ public static function _field(): ObjectType ], 'args' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), - 'resolve' => static fn (FieldDefinition $field): array => $field->args, + 'args' => [ + 'includeDeprecated' => [ + 'type' => Type::boolean(), + 'defaultValue' => false, + ], + ], + 'resolve' => static function (FieldDefinition $field, $args): array { + $values = $field->args; + + if (! ($args['includeDeprecated'] ?? false)) { + return \array_filter( + $values, + static fn (Argument $value): bool => + $value->deprecationReason === null + || $value->deprecationReason === '', + + ); + } + + return $values; + }, ], 'type' => [ 'type' => Type::nonNull(self::_type()), @@ -532,6 +576,17 @@ public static function _inputValue(): ObjectType return null; }, ], + 'isDeprecated' => [ + 'type' => Type::nonNull(Type::boolean()), + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static fn ($inputValue): bool => $inputValue->deprecationReason !== null + && $inputValue->deprecationReason !== '', + ], + 'deprecationReason' => [ + 'type' => Type::string(), + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static fn ($inputValue): ?string => $inputValue->deprecationReason, + ], ], ]); } diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 30a7e852f..1e5aee27c 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -141,6 +141,7 @@ private function makeInputValues(NodeList $values): array 'name' => $value->name->value, 'type' => $type, 'description' => $value->description->value ?? null, + 'deprecationReason' => $this->getDeprecationReason($value), 'astNode' => $value, ]; @@ -388,7 +389,7 @@ public function buildField(FieldDefinitionNode $field): array * Given a collection of directives, returns the string value for the * deprecation reason. * - * @param EnumValueDefinitionNode|FieldDefinitionNode $node + * @param EnumValueDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode $node * * @throws \Exception * @throws \ReflectionException diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index 41836e8ec..bab501b82 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -300,6 +300,7 @@ protected function extendInputFieldMap(InputObjectType $type): array $newFieldConfig = [ 'description' => $field->description, 'type' => $extendedType, + 'deprecationReason' => $field->deprecationReason, 'astNode' => $field->astNode, ]; @@ -459,6 +460,7 @@ protected function extendArgs(array $args): array $def = [ 'type' => $extendedType, 'description' => $arg->description, + 'deprecationReason' => $arg->deprecationReason, 'astNode' => $arg->astNode, ]; diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index 3eabcd1e7..07279a48e 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -336,7 +336,7 @@ protected static function printArgs(array $options, array $args, string $indenta */ protected static function printInputValue($arg): string { - $argDecl = "{$arg->name}: {$arg->getType()->toString()}"; + $argDecl = "{$arg->name}: {$arg->getType()->toString()}" . static::printDeprecated($arg); if ($arg->defaultValueExists()) { $defaultValueAST = AST::astFromValue($arg->defaultValue, $arg->getType()); @@ -424,15 +424,15 @@ protected static function printFields(array $options, $type): string } /** - * @param FieldDefinition|EnumValueDefinition $fieldOrEnumVal + * @param FieldDefinition|EnumValueDefinition|InputObjectField|Argument $deprecation * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ - protected static function printDeprecated($fieldOrEnumVal): string + protected static function printDeprecated($deprecation): string { - $reason = $fieldOrEnumVal->deprecationReason; + $reason = $deprecation->deprecationReason; if ($reason === null) { return ''; } diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index 93da39b8d..625fe1042 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -920,13 +920,19 @@ public function testAcceptsAnObjectTypeWithFieldArgs(): void 'goodField' => [ 'type' => Type::string(), 'args' => [ - 'goodArg' => ['type' => Type::string()], + 'goodArg' => [ + 'type' => Type::string(), + 'deprecationReason' => 'Just because', + ], ], ], ], ]); $objType->assertValid(); - self::assertDidNotCrash(); + $argument = $objType->getField('goodField')->getArg('goodArg'); + self::assertInstanceOf(Argument::class, $argument); + self::assertTrue($argument->isDeprecated()); + self::assertSame('Just because', $argument->deprecationReason); } // Object interfaces must be array @@ -1436,12 +1442,16 @@ public function testAcceptsAnInputObjectTypeWithFields(): void 'fields' => [ $fieldName => [ 'type' => Type::string(), + 'deprecationReason' => 'Just because', ], ], ]); $inputObjType->assertValid(); - self::assertSame(Type::string(), $inputObjType->getField($fieldName)->getType()); + $field = $inputObjType->getField($fieldName); + self::assertSame(Type::string(), $field->getType()); + self::assertTrue($field->isDeprecated()); + self::assertSame('Just because', $field->deprecationReason); } /** @see it('accepts an Input Object type with a field function') */ @@ -1453,12 +1463,16 @@ public function testAcceptsAnInputObjectTypeWithAFieldFunction(): void 'fields' => static fn (): array => [ $fieldName => [ 'type' => Type::string(), + 'deprecationReason' => 'Just because', ], ], ]); $inputObjType->assertValid(); + $field = $inputObjType->getField($fieldName); self::assertSame(Type::string(), $inputObjType->getField($fieldName)->getType()); + self::assertTrue($field->isDeprecated()); + self::assertSame('Just because', $field->deprecationReason); } /** @see it('accepts an Input Object type with a field type function') */ diff --git a/tests/Type/DirectiveTest.php b/tests/Type/DirectiveTest.php index dbeb7353c..fae3c7c61 100644 --- a/tests/Type/DirectiveTest.php +++ b/tests/Type/DirectiveTest.php @@ -2,8 +2,10 @@ namespace GraphQL\Tests\Type; +use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\Type; use PHPUnit\Framework\TestCase; /** @@ -11,6 +13,8 @@ */ final class DirectiveTest extends TestCase { + use ArraySubsetAsserts; + /** @see it('defines a directive with no args', () => { */ public function testDefinesADirectiveWithNoArgs(): void { @@ -28,5 +32,56 @@ public function testDefinesADirectiveWithNoArgs(): void self::assertSame($locations, $directive->locations); } + /** @see it('defines a directive with multiple args' */ + public function testDefinesADirectiveWithMultipleArgs(): void + { + $name = 'Foo'; + $locations = [DirectiveLocation::QUERY]; + + $directive = new Directive([ + 'name' => $name, + 'args' => [ + 'foo' => [ + 'type' => Type::string(), + 'deprecationReason' => 'Just because', + ], + 'bar' => [ + 'type' => Type::int(), + ], + ], + 'locations' => $locations, + ]); + + self::assertSame($name, $directive->name); + self::assertFalse($directive->isRepeatable); + self::assertSame($locations, $directive->locations); + + $argumentFoo = $directive->args[0]; + self::assertArraySubset( + [ + 'name' => 'foo', + 'description' => null, + 'deprecationReason' => 'Just because', + 'defaultValue' => null, + 'astNode' => null, + ], + (array) $argumentFoo + ); + self::assertTrue($argumentFoo->isDeprecated()); + + $argumentBar = $directive->args[1]; + self::assertArraySubset( + [ + 'name' => 'bar', + 'description' => null, + 'deprecationReason' => null, + 'defaultValue' => null, + 'astNode' => null, + ], + (array) $argumentBar + ); + self::assertFalse($argumentBar->isDeprecated()); + } + // TODO implement all of https://github.com/graphql/graphql-js/blob/master/src/type/__tests__/directive-test.js } diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index 9a528d23d..a6402d9dc 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -232,6 +232,8 @@ public function testExecutesAnIntrospectionQuery(): void 'ofType' => null, ], 'defaultValue' => 'false', + 'isDeprecated' => false, + 'deprecationReason' => null, ], ], 'type' => [ @@ -299,6 +301,8 @@ public function testExecutesAnIntrospectionQuery(): void 'ofType' => null, ], 'defaultValue' => 'false', + 'isDeprecated' => false, + 'deprecationReason' => null, ], ], 'type' => [ @@ -319,7 +323,19 @@ public function testExecutesAnIntrospectionQuery(): void ], 7 => [ 'name' => 'inputFields', - 'args' => [], + 'args' => [ + 0 => [ + 'name' => 'includeDeprecated', + 'type' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], + 'defaultValue' => 'false', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + ], 'type' => [ 'kind' => 'LIST', 'name' => null, @@ -435,7 +451,19 @@ public function testExecutesAnIntrospectionQuery(): void ], 2 => [ '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, @@ -559,6 +587,32 @@ public function testExecutesAnIntrospectionQuery(): void 'isDeprecated' => false, 'deprecationReason' => null, ], + 4 => [ + 'name' => 'isDeprecated', + 'args' => [], + 'type' => [ + 'kind' => 'NON_NULL', + 'name' => null, + 'ofType' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], + ], + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + 5 => [ + 'name' => 'deprecationReason', + 'args' => [], + 'type' => [ + 'kind' => 'SCALAR', + 'name' => 'String', + 'ofType' => null, + ], + 'isDeprecated' => false, + 'deprecationReason' => null, + ], ], 'inputFields' => null, 'interfaces' => [], @@ -846,6 +900,8 @@ public function testExecutesAnIntrospectionQuery(): void ], ], 'defaultValue' => null, + 'isDeprecated' => false, + 'deprecationReason' => null, ], ], 'isRepeatable' => false, @@ -870,6 +926,8 @@ public function testExecutesAnIntrospectionQuery(): void ], ], 'defaultValue' => null, + 'isDeprecated' => false, + 'deprecationReason' => null, ], ], 'isRepeatable' => false, @@ -890,12 +948,16 @@ public function testExecutesAnIntrospectionQuery(): void 'ofType' => null, ], 'defaultValue' => '"No longer supported"', + 'isDeprecated' => false, + 'deprecationReason' => null, ], ], 'isRepeatable' => false, 'locations' => [ 0 => 'FIELD_DEFINITION', 1 => 'ENUM_VALUE', + 2 => 'ARGUMENT_DEFINITION', + 3 => 'INPUT_FIELD_DEFINITION', ], ], ], @@ -1142,6 +1204,115 @@ public function testRespectsTheIncludeDeprecatedParameterForFields(): void self::assertSame($expected, GraphQL::executeQuery($schema, $request)->toArray()); } + /** @see it('identifies deprecated args' */ + public function testIdentifiesDeprecatedArgs(): void + { + $TestType = new ObjectType([ + 'name' => 'TestType', + 'fields' => [ + 'testField' => [ + 'type' => Type::string(), + 'args' => [ + 'nonDeprecated' => [ + 'type' => Type::string(), + ], + 'deprecated' => [ + 'type' => Type::string(), + 'deprecationReason' => 'Removed in 1.0', + ], + ], + ], + ], + ]); + + $schema = new Schema(['query' => $TestType]); + $request = ' + { + __type(name: "TestType") { + fields { + args(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + } + } + '; + + $expected = [ + [ + 'args' => [ + [ + 'name' => 'nonDeprecated', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'deprecated', + 'isDeprecated' => true, + 'deprecationReason' => 'Removed in 1.0', + ], + ], + ], + ]; + + $result = GraphQL::executeQuery($schema, $request)->toArray(); + self::assertEquals($expected, $result['data']['__type']['fields'] ?? null); + } + + /** @see it('respects the includeDeprecated parameter for args' */ + public function testRespectsTheIncludeDeprecatedParameterForArgs(): void + { + $TestType = new ObjectType([ + 'name' => 'TestType', + 'fields' => [ + 'testField' => [ + 'type' => Type::string(), + 'args' => [ + 'nonDeprecated' => [ + 'type' => Type::string(), + ], + 'deprecated' => [ + 'type' => Type::string(), + 'deprecationReason' => 'Removed in 1.0', + ], + ], + ], + ], + ]); + + $schema = new Schema(['query' => $TestType]); + $request = ' + { + __type(name: "TestType") { + fields { + trueArgs: args(includeDeprecated: true) { + name + } + falseArgs: args(includeDeprecated: false) { + name + } + omittedArgs: args { + name + } + } + } + } + '; + + $expected = [ + [ + 'trueArgs' => [['name' => 'nonDeprecated'], ['name' => 'deprecated']], + 'falseArgs' => [['name' => 'nonDeprecated']], + 'omittedArgs' => [['name' => 'nonDeprecated']], + ], + ]; + + $result = GraphQL::executeQuery($schema, $request)->toArray(); + self::assertSame($expected, $result['data']['__type']['fields'] ?? null); + } + /** @see it('identifies deprecated enum values') */ public function testIdentifiesDeprecatedEnumValues(): void { @@ -1261,6 +1432,121 @@ public function testRespectsTheIncludeDeprecatedParameterForEnumValues(): void self::assertSame($expected, GraphQL::executeQuery($schema, $request)->toArray()); } + /** @see it('identifies deprecated for input fields' */ + public function testIdentifiesDeprecatedForInputFields(): void + { + $TestInputObject = new InputObjectType([ + 'name' => 'TestInputObject', + 'fields' => [ + 'nonDeprecated' => [ + 'type' => Type::string(), + ], + 'deprecated' => [ + 'type' => Type::listOf(Type::string()), + 'deprecationReason' => 'Very important fake reason', + ], + ], + ]); + + $TestType = new ObjectType([ + 'name' => 'TestType', + 'fields' => [ + 'field' => [ + 'type' => Type::string(), + 'args' => ['complex' => ['type' => $TestInputObject]], + ], + ], + ]); + + $schema = new Schema(['query' => $TestType]); + $request = ' + { + __type(name: "TestInputObject") { + name + trueFields: inputFields(includeDeprecated: true) { + name + } + falseFields: inputFields(includeDeprecated: false) { + name + } + omittedFields: inputFields { + name + } + } + } + '; + + $expected = [ + 'name' => 'TestInputObject', + 'trueFields' => [['name' => 'nonDeprecated'], ['name' => 'deprecated']], + 'falseFields' => [['name' => 'nonDeprecated']], + 'omittedFields' => [['name' => 'nonDeprecated']], + ]; + + $result = GraphQL::executeQuery($schema, $request)->toArray(); + self::assertSame($expected, $result['data']['__type'] ?? null); + } + + /** @see it('respects the includeDeprecated parameter for input fields' */ + public function testRespectsTheIncludeDeprecatedParameterForInputFields(): void + { + $TestInputObject = new InputObjectType([ + 'name' => 'TestInputObject', + 'fields' => [ + 'nonDeprecated' => [ + 'type' => Type::string(), + ], + 'deprecated' => [ + 'type' => Type::listOf(Type::string()), + 'deprecationReason' => 'Very important fake reason', + ], + ], + ]); + + $TestType = new ObjectType([ + 'name' => 'TestType', + 'fields' => [ + 'field' => [ + 'type' => Type::string(), + 'args' => ['complex' => ['type' => $TestInputObject]], + ], + ], + ]); + + $schema = new Schema(['query' => $TestType]); + $request = ' + { + __type(name: "TestInputObject") { + name + inputFields(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + } + '; + + $expected = [ + 'name' => 'TestInputObject', + 'inputFields' => [ + [ + 'name' => 'nonDeprecated', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'deprecated', + 'isDeprecated' => true, + 'deprecationReason' => 'Very important fake reason', + ], + ], + ]; + + $result = GraphQL::executeQuery($schema, $request)->toArray(); + self::assertEquals($expected, $result['data']['__type'] ?? null); + } + /** @see it('fails as expected on the __type root field without an arg') */ public function testFailsAsExpectedOnTheTypeRootFieldWithoutAnArg(): void { diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index c254fc0ff..eacd42e19 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -1005,6 +1005,26 @@ public function testRejectsAnInputObjectTypeWithIncorrectlyTypedFields(): void ); } + /** @see it('rejects an Input Object type with required argument that is deprecated' */ + public function testRejectsAnInputObjectTypeWithRequiredArgumentThatIsDeprecated(): void + { + $schema = BuildSchema::build(' + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject { + optionalField: String @deprecated + anotherOptionalField: String! = "" @deprecated + badField: String! @deprecated + } + '); + + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Required input field SomeInputObject.badField cannot be deprecated.'); + $schema->assertValid(); + } + /** @see it('rejects an Enum type without values') */ public function testRejectsAnEnumTypeWithoutValues(): void { @@ -1544,6 +1564,29 @@ public function testRejectsANonInputTypeAsAFieldArgType(): void } } + /** @see it('rejects an required argument that is deprecated' */ + public function testRejectsARequiredArgumentThatIsDeprecated(): void + { + $schema = BuildSchema::build(' + directive @BadDirective( + badArg: String! @deprecated + optionalArg: String @deprecated + anotherOptionalArg: String! = "" @deprecated + ) on FIELD + type Query { + test( + badArg: String! @deprecated + optionalArg: String @deprecated + anotherOptionalArg: String! = "" @deprecated + ): String + } + '); + + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Required argument String.test(badArg:) cannot be deprecated.'); + $schema->assertValid(); + } + /** @see it('rejects a non-input type as a field arg with locations') */ public function testANonInputTypeAsAFieldArgWithLocations(): void { diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index a1d7477cb..1f87b5dc7 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -745,7 +745,6 @@ public function testUnreferencedTypeImplementingReferencedUnion(): void /** @see it('Supports @deprecated') */ public function testSupportsDeprecated(): void { - // TODO restore @deprecated on inputs - see https://github.com/webonyx/graphql-php/issues/110 $sdl = <<isDeprecated()); self::assertSame('Because I said so', $rootFields['field2']->deprecationReason); - self::markTestIncomplete('See https://github.com/webonyx/graphql-php/issues/110'); $type = $schema->getType('MyInput'); self::assertInstanceOf(InputObjectType::class, $type); $inputFields = $type->getFields(); diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index ae2b2ff36..4e10d5bd1 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -19,6 +19,7 @@ use GraphQL\Tests\Utils\SchemaExtenderTest\SomeObjectClassType; use GraphQL\Tests\Utils\SchemaExtenderTest\SomeScalarClassType; use GraphQL\Tests\Utils\SchemaExtenderTest\SomeUnionClassType; +use GraphQL\Type\Definition\Argument; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -662,12 +663,16 @@ public function testBuildsTypesWithDeprecatedFieldsOrValues(): void $schema = new Schema([]); $extendAST = Parser::parse(' type SomeObject { - deprecatedField: String @deprecated(reason: "not used anymore") + deprecatedField(deprecatedArg: String @deprecated(reason: "unusable")): String @deprecated(reason: "not used anymore") } enum SomeEnum { DEPRECATED_VALUE @deprecated(reason: "do not use") } + + input SomeInputObject { + deprecatedField: String @deprecated(reason: "redundant") + } '); $extendedSchema = SchemaExtender::extend($schema, $extendAST); @@ -679,6 +684,12 @@ enum SomeEnum { self::assertTrue($deprecatedFieldDef->isDeprecated()); self::assertSame('not used anymore', $deprecatedFieldDef->deprecationReason); + $deprecatedArgument = $deprecatedFieldDef->getArg('deprecatedArg'); + + self::assertInstanceOf(Argument::class, $deprecatedArgument); + self::assertTrue($deprecatedArgument->isDeprecated()); + self::assertSame('unusable', $deprecatedArgument->deprecationReason); + $someEnum = $extendedSchema->getType('SomeEnum'); assert($someEnum instanceof EnumType); @@ -687,6 +698,14 @@ enum SomeEnum { self::assertTrue($deprecatedEnumDef->isDeprecated()); self::assertSame('do not use', $deprecatedEnumDef->deprecationReason); + + $someInput = $extendedSchema->getType('SomeInputObject'); + assert($someInput instanceof InputObjectType); + + $deprecatedInputField = $someInput->getField('deprecatedField'); + + self::assertTrue($deprecatedInputField->isDeprecated()); + self::assertSame('redundant', $deprecatedInputField->deprecationReason); } /** @see it('extends objects with deprecated fields') */ diff --git a/tests/Utils/SchemaPrinterTest.php b/tests/Utils/SchemaPrinterTest.php index b559696e2..7b2cb1d2d 100644 --- a/tests/Utils/SchemaPrinterTest.php +++ b/tests/Utils/SchemaPrinterTest.php @@ -968,7 +968,7 @@ public function testPrintIntrospectionSchema(): void 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 | ENUM_VALUE + ) on FIELD_DEFINITION | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations." type __Schema { @@ -1001,7 +1001,7 @@ public function testPrintIntrospectionSchema(): void interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] + inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type } @@ -1036,7 +1036,7 @@ enum __TypeKind { type __Field { name: String! description: String - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -1050,6 +1050,9 @@ enum __TypeKind { "A GraphQL-formatted string representing the default value for this input value." defaultValue: String + + isDeprecated: Boolean! + deprecationReason: String } "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string." diff --git a/tests/Validator/QueryComplexityTest.php b/tests/Validator/QueryComplexityTest.php index adbf718a8..8ea6da018 100644 --- a/tests/Validator/QueryComplexityTest.php +++ b/tests/Validator/QueryComplexityTest.php @@ -164,7 +164,7 @@ public function testQueryWithCustomAndSkipDirective(): void public function testComplexityIntrospectionQuery(): void { - $this->assertIntrospectionQuery(181); + $this->assertIntrospectionQuery(187); } public function testIntrospectionTypeMetaFieldQuery(): void From 0a035289b14b8b22f4354e741983549801de2a50 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 11 May 2023 12:24:18 +0200 Subject: [PATCH 2/2] remove newlines --- src/Type/Introspection.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index dddcfa93a..7fb70ec01 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -410,7 +410,6 @@ static function (EnumValueDefinition $value): bool { static fn (InputObjectField $field): bool => $field->deprecationReason === null || $field->deprecationReason === '', - ); } @@ -508,7 +507,6 @@ public static function _field(): ObjectType static fn (Argument $value): bool => $value->deprecationReason === null || $value->deprecationReason === '', - ); }