diff --git a/CHANGELOG.md b/CHANGELOG.md index 843ac8052..420ba7658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,24 @@ You can find and compare releases at the [GitHub release page](https://github.co - Cache query validation results https://github.com/nuwave/lighthouse/pull/2603 +## v6.44.2 + +### Fixed + +- Apply `@convertEmptyStringsToNull` to input fields when used upon fields https://github.com/nuwave/lighthouse/issues/2610 + +## v6.44.1 + +### Fixed + +- Ensure `deprecationReason` is set on arguments and input fields https://github.com/nuwave/lighthouse/pull/2609 + +## v6.44.0 + +### Added + +- Allow `@deprecated` directive on arguments and input fields https://github.com/nuwave/lighthouse/pull/2607 + ## v6.43.1 ### Changed diff --git a/docs/6/api-reference/directives.md b/docs/6/api-reference/directives.md index 50db50e59..8129321f2 100644 --- a/docs/6/api-reference/directives.md +++ b/docs/6/api-reference/directives.md @@ -896,7 +896,10 @@ final class ComplexityAnalyzer ```graphql """ -Replaces `""` with `null`. +Replaces incoming empty strings `""` with `null`. + +When used upon fields, empty strings for non-nullable inputs will pass unchanged. +Only explicitly placing this on non-nullable inputs will force the conversion. """ directive @convertEmptyStringsToNull on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION ``` @@ -1165,17 +1168,16 @@ 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 in [Markdown](https://commonmark.org). + Explains why this element was deprecated. + It is also beneficial to suggest what to use instead. + Formatted in Markdown, as specified by [CommonMark](https://commonmark.org). """ reason: String = "No longer supported" -) on FIELD_DEFINITION | ENUM_VALUE +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE ``` -You can mark fields as deprecated by adding the [@deprecated](#deprecated) directive. -It is recommended to provide a `reason` for the deprecation, as well as a suggestion on -how to move forward. +You can indicate schema elements are no longer supported by adding the [@deprecated](#deprecated) directive. +It is recommended to provide a `reason` for the deprecation, as well as suggest a replacement. ```graphql type Query { diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index 50db50e59..8129321f2 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -896,7 +896,10 @@ final class ComplexityAnalyzer ```graphql """ -Replaces `""` with `null`. +Replaces incoming empty strings `""` with `null`. + +When used upon fields, empty strings for non-nullable inputs will pass unchanged. +Only explicitly placing this on non-nullable inputs will force the conversion. """ directive @convertEmptyStringsToNull on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION ``` @@ -1165,17 +1168,16 @@ 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 in [Markdown](https://commonmark.org). + Explains why this element was deprecated. + It is also beneficial to suggest what to use instead. + Formatted in Markdown, as specified by [CommonMark](https://commonmark.org). """ reason: String = "No longer supported" -) on FIELD_DEFINITION | ENUM_VALUE +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE ``` -You can mark fields as deprecated by adding the [@deprecated](#deprecated) directive. -It is recommended to provide a `reason` for the deprecation, as well as a suggestion on -how to move forward. +You can indicate schema elements are no longer supported by adding the [@deprecated](#deprecated) directive. +It is recommended to provide a `reason` for the deprecation, as well as suggest a replacement. ```graphql type Query { diff --git a/phpstan.neon b/phpstan.neon index b6d259e77..02d1c6d32 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -46,7 +46,7 @@ parameters: - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder|Illuminate\\Database\\Eloquent\\Relations\\Relation|Illuminate\\Database\\Query\\Builder::(orderBy|where|whereIn|whereNotIn|whereBetween|whereJsonContains|whereNotBetween)\(\)\.#' # Laravel 11 added generics that are handled differently, so we just omit them - - '#generic class Illuminate\\Database\\Eloquent\\Builder but does not specify its types#' + - '#generic class (Illuminate\\Database\\Eloquent\\Builder|Laravel\\Scout\\Builder)( but)? does not specify its types#' # This test cheats and uses reflection to make assertions - path: tests/Unit/Schema/Directives/BaseDirectiveTest.php diff --git a/src/Schema/AST/ASTHelper.php b/src/Schema/AST/ASTHelper.php index 0e41037d6..4f19ad136 100644 --- a/src/Schema/AST/ASTHelper.php +++ b/src/Schema/AST/ASTHelper.php @@ -327,7 +327,7 @@ public static function qualifiedArgType( } /** Given a collection of directives, returns the string value for the deprecation reason. */ - public static function deprecationReason(EnumValueDefinitionNode|FieldDefinitionNode $node): ?string + public static function deprecationReason(EnumValueDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode $node): ?string { $deprecated = Values::getDirectiveValues( DirectiveDefinition::deprecatedDirective(), diff --git a/src/Schema/Directives/ConvertEmptyStringsToNullDirective.php b/src/Schema/Directives/ConvertEmptyStringsToNullDirective.php index 8b9f19645..ea0b9dfd5 100644 --- a/src/Schema/Directives/ConvertEmptyStringsToNullDirective.php +++ b/src/Schema/Directives/ConvertEmptyStringsToNullDirective.php @@ -16,7 +16,10 @@ public static function definition(): string { return /** @lang GraphQL */ <<<'GRAPHQL' """ -Replaces `""` with `null`. +Replaces incoming empty strings `""` with `null`. + +When used upon fields, empty strings for non-nullable inputs will pass unchanged. +Only explicitly placing this on non-nullable inputs will force the conversion. """ directive @convertEmptyStringsToNull on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION GRAPHQL; @@ -41,12 +44,13 @@ protected function transformArgumentSet(ArgumentSet $argumentSet): ArgumentSet { foreach ($argumentSet->arguments as $argument) { $namedType = $argument->namedType(); - if ( - $namedType !== null + $argumentValue = $argument->value; + + $isNullableStringType = $namedType !== null && $namedType->name === ScalarType::STRING - && ! $namedType->nonNull - ) { - $argument->value = $this->sanitize($argument->value); + && ! $namedType->nonNull; + if ($isNullableStringType || $argumentValue instanceof ArgumentSet) { + $argument->value = $this->sanitize($argumentValue); } } @@ -60,10 +64,8 @@ protected function transformArgumentSet(ArgumentSet $argumentSet): ArgumentSet */ protected function transformLeaf(mixed $value): mixed { - if ($value === '') { - return null; - } - - return $value; + return $value === '' + ? null + : $value; } } diff --git a/src/Schema/Directives/DeprecatedDirective.php b/src/Schema/Directives/DeprecatedDirective.php index 8607233ca..76e2fef2d 100644 --- a/src/Schema/Directives/DeprecatedDirective.php +++ b/src/Schema/Directives/DeprecatedDirective.php @@ -14,12 +14,12 @@ public static function definition(): string """ directive @deprecated( """ - Explains why this element was deprecated, usually also including a - suggestion for how to access supported similar data. Formatted - in [Markdown](https://daringfireball.net/projects/markdown). + Explains why this element was deprecated. + It is also beneficial to suggest what to use instead. + Formatted in Markdown, as specified by [CommonMark](https://commonmark.org). """ reason: String = "No longer supported" -) on FIELD_DEFINITION | ENUM_VALUE +) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE GRAPHQL; } } diff --git a/src/Schema/Factories/ArgumentFactory.php b/src/Schema/Factories/ArgumentFactory.php index b27ea469c..d3bab6e1d 100644 --- a/src/Schema/Factories/ArgumentFactory.php +++ b/src/Schema/Factories/ArgumentFactory.php @@ -50,6 +50,7 @@ public function convert(InputValueDefinitionNode $definitionNode): array 'name' => $definitionNode->name->value, 'description' => $definitionNode->description?->value, 'type' => $type, + 'deprecationReason' => ASTHelper::deprecationReason($definitionNode), 'astNode' => $definitionNode, ]; diff --git a/tests/Integration/Schema/Directives/ConvertEmptyStringsToNullDirectiveTest.php b/tests/Integration/Schema/Directives/ConvertEmptyStringsToNullDirectiveTest.php index b4aa9caf3..62710a739 100644 --- a/tests/Integration/Schema/Directives/ConvertEmptyStringsToNullDirectiveTest.php +++ b/tests/Integration/Schema/Directives/ConvertEmptyStringsToNullDirectiveTest.php @@ -2,6 +2,7 @@ namespace Tests\Integration\Schema\Directives; +use Nuwave\Lighthouse\Schema\Directives\ConvertEmptyStringsToNullDirective; use Tests\TestCase; final class ConvertEmptyStringsToNullDirectiveTest extends TestCase @@ -159,4 +160,154 @@ public function testConvertsNonNullableArgumentsWhenUsedOnArgument(): void ], ]); } + + public function testConvertsEmptyStringToNullWithFieldDirective(): void + { + $this->schema = /** @lang GraphQL */ ' + type Query { + foo(bar: String): FooResponse + @convertEmptyStringsToNull + @field(resolver: "Tests\\\Utils\\\Mutations\\\ReturnReceivedInput") + } + + type FooResponse { + bar: String + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo(bar: "") { + bar + } + } + ')->assertExactJson([ + 'data' => [ + 'foo' => [ + 'bar' => null, + ], + ], + ]); + } + + public function testConvertsEmptyStringToNullWithGlobalFieldMiddleware(): void + { + config(['lighthouse.field_middleware' => [ + ConvertEmptyStringsToNullDirective::class, + ]]); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo(bar: String): FooResponse + @field(resolver: "Tests\\\Utils\\\Mutations\\\ReturnReceivedInput") + } + + type FooResponse { + bar: String + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo(bar: "") { + bar + } + } + ')->assertExactJson([ + 'data' => [ + 'foo' => [ + 'bar' => null, + ], + ], + ]); + } + + public function testConvertsEmptyStringToNullWithFieldDirectiveAndInputType(): void + { + $this->schema = /** @lang GraphQL */ ' + type Query { + foo(input: FooInput): FooInputResponse + @convertEmptyStringsToNull + @field(resolver: "Tests\\\Utils\\\Mutations\\\ReturnReceivedInput") + } + + input FooInput { + bar: String + } + + type FooInputResponse { + input: FooResponse + } + + type FooResponse { + bar: String + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo(input: { + bar: "" + }) { + input { + bar + } + } + } + ')->assertExactJson([ + 'data' => [ + 'foo' => [ + 'input' => [ + 'bar' => null, + ], + ], + ], + ]); + } + + public function testConvertsEmptyStringToNullWithGlobalFieldMiddlewareAndInputType(): void + { + config(['lighthouse.field_middleware' => [ + ConvertEmptyStringsToNullDirective::class, + ]]); + + $this->schema = /** @lang GraphQL */ ' + type Query { + foo(input: FooInput): FooInputResponse + @field(resolver: "Tests\\\Utils\\\Mutations\\\ReturnReceivedInput") + } + + input FooInput { + bar: String + } + + type FooInputResponse { + input: FooResponse + } + + type FooResponse { + bar: String + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + { + foo(input: { + bar: "" + }) { + input { + bar + } + } + } + ')->assertExactJson([ + 'data' => [ + 'foo' => [ + 'input' => [ + 'bar' => null, + ], + ], + ], + ]); + } } diff --git a/tests/Utils/Mutations/ReturnReceivedInput.php b/tests/Utils/Mutations/ReturnReceivedInput.php new file mode 100644 index 000000000..bf9a5ca0e --- /dev/null +++ b/tests/Utils/Mutations/ReturnReceivedInput.php @@ -0,0 +1,16 @@ + $args + * + * @return array + */ + public function __invoke(mixed $root, array $args): array + { + return $args; + } +}