diff --git a/packages/graphql/UPGRADE.md b/packages/graphql/UPGRADE.md index 66d0e933d..038dc403d 100644 --- a/packages/graphql/UPGRADE.md +++ b/packages/graphql/UPGRADE.md @@ -51,6 +51,21 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases) * [ ] If you are testing generated queries, you need to update `sort_by_*` alias to `lara_asp_graphql__sort_by__*`. +* [ ] If you are overriding Extra operators, you may need to add `SortByOperators::Extra` to use new built-in: + + ```php + $settings = [ + 'sort_by' => [ + 'operators' => [ + SortByOperators::Extra => [ + SortByOperators::Extra, + SortByOperatorRandomDirective::class, + ], + ], + ], + ]; + ``` + ## API This section is actual only if you are extending the package. Please review and update (listed the most significant changes only): @@ -73,6 +88,6 @@ This section is actual only if you are extending the package. Please review and * [ ] To get `BuilderInfo` instance within Operator the `LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Context` should be used instead of `LastDragon_ru\LaraASP\GraphQL\Builder\Manipulator`: - ```php - $context->get(\LastDragon_ru\LaraASP\GraphQL\Builder\Contexts\AstManipulation\AstManipulation::class)?->builderInfo - ``` + ```php + $context->get(\LastDragon_ru\LaraASP\GraphQL\Builder\Contexts\AstManipulation\AstManipulation::class)?->builderInfo + ``` diff --git a/packages/graphql/docs/Directives/@sortBy.md b/packages/graphql/docs/Directives/@sortBy.md index f633b4e7f..a1d4f2764 100644 --- a/packages/graphql/docs/Directives/@sortBy.md +++ b/packages/graphql/docs/Directives/@sortBy.md @@ -197,3 +197,15 @@ $settings = [ return $settings; ``` + +The query is also supported and have highest priority (will override default settings): + +```graphql +query { + # ORDER BY user.name ASC NULLS FIRST, text DESC + comments(order: [ + {nullsFirst: {user: {name: asc}}} + {text: desc} + ]) +} +``` diff --git a/packages/graphql/src/Builder/Context.php b/packages/graphql/src/Builder/Context.php index 6a22f4148..9a8b8eecc 100644 --- a/packages/graphql/src/Builder/Context.php +++ b/packages/graphql/src/Builder/Context.php @@ -10,7 +10,7 @@ */ class Context implements ContextContract { /** - * @var array + * @var array */ private array $context = []; @@ -38,7 +38,11 @@ public function override(array $context): static { $overridden = clone $this; foreach ($context as $key => $value) { - $overridden->context[$key] = $value; + if ($value !== null) { + $overridden->context[$key] = $value; + } else { + unset($overridden->context[$key]); + } } return $overridden; diff --git a/packages/graphql/src/Builder/ContextTest.php b/packages/graphql/src/Builder/ContextTest.php new file mode 100644 index 000000000..d5451b422 --- /dev/null +++ b/packages/graphql/src/Builder/ContextTest.php @@ -0,0 +1,36 @@ +has($class)); + self::assertNull($context->get($class)); + + $overridden = $context->override([$class => new $class('overridden')]); + + self::assertNotSame($context, $overridden); + self::assertFalse($context->has($class)); + self::assertNull($context->get($class)); + self::assertTrue($overridden->has($class)); + self::assertNotNull($overridden->get($class)); + self::assertEquals('overridden', $overridden->get($class)->value); + } +} diff --git a/packages/graphql/src/Builder/Directives/PropertyDirective.php b/packages/graphql/src/Builder/Directives/PropertyDirective.php index ccf72ee5c..cda2965a6 100644 --- a/packages/graphql/src/Builder/Directives/PropertyDirective.php +++ b/packages/graphql/src/Builder/Directives/PropertyDirective.php @@ -6,18 +6,14 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Handler; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeProvider; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeSource; -use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\Client\ConditionEmpty; -use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\Client\ConditionTooManyOperators; -use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\HandlerInvalidConditions; use LastDragon_ru\LaraASP\GraphQL\Builder\Property; -use LastDragon_ru\LaraASP\GraphQL\Utils\ArgumentFactory; +use LastDragon_ru\LaraASP\GraphQL\Builder\Traits\PropertyOperator; use Nuwave\Lighthouse\Execution\Arguments\Argument; -use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet; use Override; -use function count; - abstract class PropertyDirective extends OperatorDirective { + use PropertyOperator; + #[Override] public static function getName(): string { return 'property'; @@ -41,23 +37,6 @@ public function call( Argument $argument, Context $context, ): object { - if (!($argument->value instanceof ArgumentSet)) { - throw new HandlerInvalidConditions($handler); - } - - // Empty? - if (count($argument->value->arguments) === 0) { - throw new ConditionEmpty(); - } - - // Valid? - if (count($argument->value->arguments) > 1) { - throw new ConditionTooManyOperators( - ArgumentFactory::getArgumentsNames($argument->value), - ); - } - - // Apply - return $handler->handle($builder, $property, $argument->value, $context); + return $this->handle($handler, $builder, $property, $argument, $context); } } diff --git a/packages/graphql/src/Builder/Traits/PropertyOperator.php b/packages/graphql/src/Builder/Traits/PropertyOperator.php new file mode 100644 index 000000000..4a1395fde --- /dev/null +++ b/packages/graphql/src/Builder/Traits/PropertyOperator.php @@ -0,0 +1,51 @@ +value instanceof ArgumentSet)) { + throw new HandlerInvalidConditions($handler); + } + + // Empty? + if (count($argument->value->arguments) === 0) { + throw new ConditionEmpty(); + } + + // Valid? + if (count($argument->value->arguments) > 1) { + throw new ConditionTooManyOperators( + ArgumentFactory::getArgumentsNames($argument->value), + ); + } + + // Apply + return $handler->handle($builder, $property, $argument->value, $context); + } +} diff --git a/packages/graphql/src/SortBy/Definitions/SortByOperatorNullsFirstDirective.php b/packages/graphql/src/SortBy/Definitions/SortByOperatorNullsFirstDirective.php new file mode 100644 index 000000000..ddae91ae1 --- /dev/null +++ b/packages/graphql/src/SortBy/Definitions/SortByOperatorNullsFirstDirective.php @@ -0,0 +1,11 @@ + [ + Operators::Extra => [ + // empty + ], + ], + ]); + $type = new ObjectType([ 'name' => 'TestType', 'fields' => [ @@ -390,6 +398,7 @@ static function (): void { config([ "{$package}.sort_by.operators" => [ Operators::Extra => [ + Operators::Extra, SortByOperatorRandomDirective::class, ], ], @@ -544,6 +553,58 @@ static function (): void { ]); }, ], + 'nullsFirst' => [ + [ + 'query' => <<<'SQL' + select + * + from + "test_objects" + order by + "id" DESC NULLS FIRST, + "renamed" asc + SQL + , + 'bindings' => [], + ], + [ + [ + 'nullsFirst' => [ + 'id' => 'desc', + ], + ], + [ + 'value' => 'asc', + ], + ], + null, + ], + 'nullsLast' => [ + [ + 'query' => <<<'SQL' + select + * + from + "test_objects" + order by + "id" ASC NULLS LAST, + "renamed" desc + SQL + , + 'bindings' => [], + ], + [ + [ + 'nullsLast' => [ + 'id' => 'Asc', + ], + ], + [ + 'value' => 'Desc', + ], + ], + null, + ], ]), ))->getData(); } diff --git a/packages/graphql/src/SortBy/Directives/DirectiveTest~example-expected.graphql b/packages/graphql/src/SortBy/Directives/DirectiveTest~example-expected.graphql index 4bdc2e5ae..1000cc8cc 100644 --- a/packages/graphql/src/SortBy/Directives/DirectiveTest~example-expected.graphql +++ b/packages/graphql/src/SortBy/Directives/DirectiveTest~example-expected.graphql @@ -11,6 +11,18 @@ on | INPUT_FIELD_DEFINITION | SCALAR +directive @sortByOperatorNullsFirst +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + +directive @sortByOperatorNullsLast +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + directive @sortByOperatorProperty on | ENUM @@ -39,6 +51,18 @@ enum SortByTypeDirection { Sort clause for `type Comment` (only one property allowed at a time). """ input SortByClauseComment { + """ + NULLs first + """ + nullsFirst: SortByClauseComment + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseComment + @sortByOperatorNullsLast + """ Property clause. """ @@ -67,6 +91,18 @@ input SortByClauseUser { """ name: SortByTypeDirection @sortByOperatorField + + """ + NULLs first + """ + nullsFirst: SortByClauseUser + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseUser + @sortByOperatorNullsLast } """ @@ -84,6 +120,18 @@ input SortByClauseUsersSort { """ name: SortByTypeDirection @sortByOperatorField + + """ + NULLs first + """ + nullsFirst: SortByClauseUsersSort + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseUsersSort + @sortByOperatorNullsLast } type Comment { diff --git a/packages/graphql/src/SortBy/Directives/DirectiveTest~full-expected.graphql b/packages/graphql/src/SortBy/Directives/DirectiveTest~full-expected.graphql index f38b821b5..213c46714 100644 --- a/packages/graphql/src/SortBy/Directives/DirectiveTest~full-expected.graphql +++ b/packages/graphql/src/SortBy/Directives/DirectiveTest~full-expected.graphql @@ -23,6 +23,18 @@ on | INPUT_FIELD_DEFINITION | SCALAR +directive @sortByOperatorNullsFirst +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + +directive @sortByOperatorNullsLast +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + directive @sortByOperatorProperty on | ENUM @@ -84,6 +96,18 @@ input SortByClauseNested { nested: SortByClauseNested @sortByOperatorProperty + """ + NULLs first + """ + nullsFirst: SortByClauseNested + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseNested + @sortByOperatorNullsLast + """ By random """ @@ -164,6 +188,18 @@ input SortByClauseObject { nestedNotNull: SortByClauseObjectNested @sortByOperatorProperty + """ + NULLs first + """ + nullsFirst: SortByClauseObject + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseObject + @sortByOperatorNullsLast + """ By random """ @@ -238,6 +274,18 @@ input SortByClauseObjectInterface { nestedNotNull: SortByClauseObjectNested @sortByOperatorProperty + """ + NULLs first + """ + nullsFirst: SortByClauseObjectInterface + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseObjectInterface + @sortByOperatorNullsLast + """ By random """ @@ -255,6 +303,18 @@ input SortByClauseObjectNested { nested: SortByClauseObjectNested @sortByOperatorProperty + """ + NULLs first + """ + nullsFirst: SortByClauseObjectNested + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseObjectNested + @sortByOperatorNullsLast + """ By random """ @@ -335,6 +395,18 @@ input SortByClauseProperties { nestedNotNull: SortByClauseNested @sortByOperatorProperty + """ + NULLs first + """ + nullsFirst: SortByClauseProperties + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseProperties + @sortByOperatorNullsLast + """ By random """ @@ -397,6 +469,18 @@ input SortByQueryClauseProperties { idScalarNotNull: SortByTypeDirection @sortByOperatorField + """ + NULLs first + """ + nullsFirst: SortByQueryClauseProperties + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByQueryClauseProperties + @sortByOperatorNullsLast + """ By random """ @@ -414,6 +498,18 @@ input SortByScoutClauseNested { nested: SortByScoutClauseNested @sortByOperatorProperty + """ + NULLs first + """ + nullsFirst: SortByScoutClauseNested + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByScoutClauseNested + @sortByOperatorNullsLast + """ Property clause. """ @@ -487,6 +583,18 @@ input SortByScoutClauseProperties { """ nestedNotNull: SortByScoutClauseNested @sortByOperatorProperty + + """ + NULLs first + """ + nullsFirst: SortByScoutClauseProperties + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByScoutClauseProperties + @sortByOperatorNullsLast } interface Eloquent diff --git a/packages/graphql/src/SortBy/Directives/DirectiveTest~registry-expected.graphql b/packages/graphql/src/SortBy/Directives/DirectiveTest~registry-expected.graphql index 0db69aa1a..19f4a1c05 100644 --- a/packages/graphql/src/SortBy/Directives/DirectiveTest~registry-expected.graphql +++ b/packages/graphql/src/SortBy/Directives/DirectiveTest~registry-expected.graphql @@ -11,6 +11,18 @@ on | INPUT_FIELD_DEFINITION | SCALAR +directive @sortByOperatorNullsFirst +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + +directive @sortByOperatorNullsLast +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + directive @sortByOperatorProperty on | ENUM @@ -50,6 +62,18 @@ input SortByClauseA { """ name: SortByTypeDirection @sortByOperatorField + + """ + NULLs first + """ + nullsFirst: SortByClauseA + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseA + @sortByOperatorNullsLast } """ @@ -67,6 +91,18 @@ input SortByClauseB { """ name: SortByTypeDirection @sortByOperatorField + + """ + NULLs first + """ + nullsFirst: SortByClauseB + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseB + @sortByOperatorNullsLast } """ @@ -84,6 +120,18 @@ input SortByClauseC { """ name: SortByTypeDirection @sortByOperatorField + + """ + NULLs first + """ + nullsFirst: SortByClauseC + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseC + @sortByOperatorNullsLast } """ @@ -95,6 +143,18 @@ input SortByClauseD { """ child: SortByClauseC @sortByOperatorProperty + + """ + NULLs first + """ + nullsFirst: SortByClauseD + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseD + @sortByOperatorNullsLast } type C { diff --git a/packages/graphql/src/SortBy/Operators.php b/packages/graphql/src/SortBy/Operators.php index ea486862f..3c2bbd80b 100644 --- a/packages/graphql/src/SortBy/Operators.php +++ b/packages/graphql/src/SortBy/Operators.php @@ -5,6 +5,8 @@ use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator as BuilderOperator; use LastDragon_ru\LaraASP\GraphQL\Builder\Operators as BuilderOperators; use LastDragon_ru\LaraASP\GraphQL\Package; +use LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByOperatorNullsFirstDirective; +use LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByOperatorNullsLastDirective; use LastDragon_ru\LaraASP\GraphQL\SortBy\Directives\Directive; use Override; @@ -13,6 +15,16 @@ class Operators extends BuilderOperators { public const Extra = Directive::Name.'Extra'; + /** + * @inheritDoc + */ + protected array $default = [ + self::Extra => [ + SortByOperatorNullsFirstDirective::class, + SortByOperatorNullsLastDirective::class, + ], + ]; + public function __construct() { /** @var array|string>> $operators */ $operators = (array) config(Package::Name.'.sort_by.operators'); diff --git a/packages/graphql/src/SortBy/Operators/BaseOperator.php b/packages/graphql/src/SortBy/Operators/BaseOperator.php index 3a14bb17e..f1c933c46 100644 --- a/packages/graphql/src/SortBy/Operators/BaseOperator.php +++ b/packages/graphql/src/SortBy/Operators/BaseOperator.php @@ -2,9 +2,18 @@ namespace LastDragon_ru\LaraASP\GraphQL\SortBy\Operators; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\Query\Builder as QueryBuilder; use LastDragon_ru\LaraASP\GraphQL\Builder\Directives\OperatorDirective; use LastDragon_ru\LaraASP\GraphQL\SortBy\Contracts\Operator; +use Override; + +use function is_a; abstract class BaseOperator extends OperatorDirective implements Operator { - // empty + #[Override] + public function isBuilderSupported(string $builder): bool { + return is_a($builder, EloquentBuilder::class, true) + || is_a($builder, QueryBuilder::class, true); + } } diff --git a/packages/graphql/src/SortBy/Operators/Extra/NullsFirst.php b/packages/graphql/src/SortBy/Operators/Extra/NullsFirst.php new file mode 100644 index 000000000..38746d9b5 --- /dev/null +++ b/packages/graphql/src/SortBy/Operators/Extra/NullsFirst.php @@ -0,0 +1,63 @@ + $factory + */ + public function __construct( + protected readonly SorterFactory $factory, + ) { + parent::__construct(); + } + + #[Override] + public static function getName(): string { + return 'nullsFirst'; + } + + #[Override] + public function getFieldType(TypeProvider $provider, TypeSource $source, Context $context): string { + return $provider->getType(Clause::class, $source, $context); + } + + #[Override] + public function getFieldDescription(): string { + return 'NULLs first'; + } + + #[Override] + public function isBuilderSupported(string $builder): bool { + return $this->factory->isSupported($builder); + } + + #[Override] + public function call( + Handler $handler, + object $builder, + Property $property, + Argument $argument, + Context $context, + ): object { + return $this->handle($handler, $builder, $property->getParent(), $argument, $context->override([ + FieldContextNulls::class => new FieldContextNulls(Nulls::First), + ])); + } +} diff --git a/packages/graphql/src/SortBy/Operators/Extra/NullsFirstTest.php b/packages/graphql/src/SortBy/Operators/Extra/NullsFirstTest.php new file mode 100644 index 000000000..f9f556f93 --- /dev/null +++ b/packages/graphql/src/SortBy/Operators/Extra/NullsFirstTest.php @@ -0,0 +1,94 @@ + + // ========================================================================= + /** + * @dataProvider dataProviderCall + * + * @param array{query: string, bindings: array} $expected + * @param BuilderFactory $builderFactory + * @param Closure(static): Argument $argumentFactory + */ + public function testCall( + array $expected, + Closure $builderFactory, + Property $property, + Closure $argumentFactory, + ): void { + $operator = Container::getInstance()->make(NullsFirst::class); + $property = $property->getChild('operator name should be ignored'); + $argument = $argumentFactory($this); + $context = new Context(); + $handler = Container::getInstance()->make(Directive::class); + $builder = $builderFactory($this); + $builder = $operator->call($handler, $builder, $property, $argument, $context); + + self::assertDatabaseQueryEquals($expected, $builder); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public static function dataProviderCall(): array { + return (new CompositeDataProvider( + new BuilderDataProvider(), + new ArrayDataProvider([ + 'property' => [ + [ + 'query' => 'select * from "test_objects" order by "a" DESC NULLS FIRST', + 'bindings' => [], + ], + new Property(), + static function (self $test): Argument { + $test->useGraphQLSchema( + <<<'GRAPHQL' + type Query { + test(input: Test @sortBy): String! @all + } + + input Test { + a: String + } + GRAPHQL, + ); + + return $test->getGraphQLArgument( + 'SortByClauseTest!', + [ + 'nullsFirst' => [ + 'a' => Direction::Desc, + ], + ], + ); + }, + ], + ]), + ))->getData(); + } + // +} diff --git a/packages/graphql/src/SortBy/Operators/Extra/NullsLast.php b/packages/graphql/src/SortBy/Operators/Extra/NullsLast.php new file mode 100644 index 000000000..aa1e8e22e --- /dev/null +++ b/packages/graphql/src/SortBy/Operators/Extra/NullsLast.php @@ -0,0 +1,63 @@ + $factory + */ + public function __construct( + protected readonly SorterFactory $factory, + ) { + parent::__construct(); + } + + #[Override] + public static function getName(): string { + return 'nullsLast'; + } + + #[Override] + public function getFieldType(TypeProvider $provider, TypeSource $source, Context $context): string { + return $provider->getType(Clause::class, $source, $context); + } + + #[Override] + public function getFieldDescription(): string { + return 'NULLs last'; + } + + #[Override] + public function isBuilderSupported(string $builder): bool { + return $this->factory->isSupported($builder); + } + + #[Override] + public function call( + Handler $handler, + object $builder, + Property $property, + Argument $argument, + Context $context, + ): object { + return $this->handle($handler, $builder, $property->getParent(), $argument, $context->override([ + FieldContextNulls::class => new FieldContextNulls(Nulls::Last), + ])); + } +} diff --git a/packages/graphql/src/SortBy/Operators/Extra/NullsLastTest.php b/packages/graphql/src/SortBy/Operators/Extra/NullsLastTest.php new file mode 100644 index 000000000..bdd0dd7d2 --- /dev/null +++ b/packages/graphql/src/SortBy/Operators/Extra/NullsLastTest.php @@ -0,0 +1,94 @@ + + // ========================================================================= + /** + * @dataProvider dataProviderCall + * + * @param array{query: string, bindings: array} $expected + * @param BuilderFactory $builderFactory + * @param Closure(static): Argument $argumentFactory + */ + public function testCall( + array $expected, + Closure $builderFactory, + Property $property, + Closure $argumentFactory, + ): void { + $operator = Container::getInstance()->make(NullsLast::class); + $property = $property->getChild('operator name should be ignored'); + $argument = $argumentFactory($this); + $context = new Context(); + $handler = Container::getInstance()->make(Directive::class); + $builder = $builderFactory($this); + $builder = $operator->call($handler, $builder, $property, $argument, $context); + + self::assertDatabaseQueryEquals($expected, $builder); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public static function dataProviderCall(): array { + return (new CompositeDataProvider( + new BuilderDataProvider(), + new ArrayDataProvider([ + 'property' => [ + [ + 'query' => 'select * from "test_objects" order by "a" ASC NULLS LAST', + 'bindings' => [], + ], + new Property(), + static function (self $test): Argument { + $test->useGraphQLSchema( + <<<'GRAPHQL' + type Query { + test(input: Test @sortBy): String! @all + } + + input Test { + a: String + } + GRAPHQL, + ); + + return $test->getGraphQLArgument( + 'SortByClauseTest!', + [ + 'nullsLast' => [ + 'a' => Direction::Asc, + ], + ], + ); + }, + ], + ]), + ))->getData(); + } + // +} diff --git a/packages/graphql/src/SortBy/Operators/Extra/Random.php b/packages/graphql/src/SortBy/Operators/Extra/Random.php index cc3b24d6e..777bc3175 100644 --- a/packages/graphql/src/SortBy/Operators/Extra/Random.php +++ b/packages/graphql/src/SortBy/Operators/Extra/Random.php @@ -17,7 +17,6 @@ use Override; use function array_merge; -use function is_a; class Random extends BaseOperator { // @@ -50,12 +49,6 @@ public function getFieldType(TypeProvider $provider, TypeSource $source, Context return $provider->getType(Flag::class, $source, $context); } - #[Override] - public function isBuilderSupported(string $builder): bool { - return is_a($builder, EloquentBuilder::class, true) - || is_a($builder, QueryBuilder::class, true); - } - #[Override] public function call( Handler $handler, diff --git a/packages/graphql/src/SortBy/Operators/Field.php b/packages/graphql/src/SortBy/Operators/Field.php index 3d0efab1e..96a06cc09 100644 --- a/packages/graphql/src/SortBy/Operators/Field.php +++ b/packages/graphql/src/SortBy/Operators/Field.php @@ -62,7 +62,7 @@ public function call( if ($sorter) { $direction = $argument->value instanceof Direction ? $argument->value : Direction::Asc; - $nulls = $this->getNulls($sorter, $property, $direction); + $nulls = $this->getNulls($sorter, $context, $direction); $sorter->sort($builder, $property, $direction, $nulls); } else { @@ -75,12 +75,17 @@ public function call( /** * @param Sorter $sorter */ - protected function getNulls(Sorter $sorter, Property $property, Direction $direction): ?Nulls { + protected function getNulls(Sorter $sorter, Context $context, Direction $direction): ?Nulls { // Sortable? if (!$sorter->isNullsSupported()) { return null; } + // Explicit? + if ($context->has(FieldContextNulls::class)) { + return $context->get(FieldContextNulls::class)?->value; + } + // Default $nulls = null; $config = config(Package::Name.'.sort_by.nulls'); diff --git a/packages/graphql/src/SortBy/Operators/FieldContextNulls.php b/packages/graphql/src/SortBy/Operators/FieldContextNulls.php new file mode 100644 index 000000000..6e0121c85 --- /dev/null +++ b/packages/graphql/src/SortBy/Operators/FieldContextNulls.php @@ -0,0 +1,13 @@ +} $expected * @param BuilderFactory $builderFactory * @param Closure(static): Argument $argumentFactory + * @param Closure(static): Context $contextFactory */ public function testCall( array $expected, Closure $builderFactory, Property $property, Closure $argumentFactory, + Closure $contextFactory, ): void { $operator = Container::getInstance()->make(Field::class); $argument = $argumentFactory($this); $directive = Container::getInstance()->make(Directive::class); - $context = new Context(); + $context = $contextFactory($this); $builder = $builderFactory($this); $builder = $operator->call($directive, $builder, $property, $argument, $context); @@ -148,19 +150,26 @@ public function testCallScoutBuilder(): void { * * @param array $config * @param Closure(static): Sorter $sorterFactory + * @param Closure(static): Context $contextFactory */ - public function testGetNulls(?Nulls $expected, ?array $config, Closure $sorterFactory, Direction $direction): void { + public function testGetNulls( + ?Nulls $expected, + ?array $config, + Closure $sorterFactory, + Closure $contextFactory, + Direction $direction, + ): void { if ($config) { config($config); } $sorter = $sorterFactory($this); - $property = new Property(); + $context = $contextFactory($this); $operator = Mockery::mock(Field::class); $operator->shouldAllowMockingProtectedMethods(); $operator->makePartial(); - self::assertSame($expected, $operator->getNulls($sorter, $property, $direction)); + self::assertSame($expected, $operator->getNulls($sorter, $context, $direction)); } // @@ -193,23 +202,48 @@ public static function dataProviderCall(): array { return (new CompositeDataProvider( new BuilderDataProvider(), new ArrayDataProvider([ - 'property' => [ + 'property' => [ [ 'query' => 'select * from "test_objects" order by "a" desc', 'bindings' => [], ], new Property('a'), $factory, + static function (): Context { + return new Context(); + }, + ], + 'nulls from Context' => [ + [ + 'query' => 'select * from "test_objects" order by "a" DESC NULLS FIRST', + 'bindings' => [], + ], + new Property('a'), + $factory, + static function (): Context { + return (new Context())->override([ + FieldContextNulls::class => new FieldContextNulls(Nulls::First), + ]); + }, ], ]), ))->getData(); } /** - * @return array, Closure(static): Sorter, Direction}> + * @return array, + * Closure(static): Sorter, + * Closure(static): Context, + * Direction, + * }> */ public static function dataProviderGetNulls(): array { $key = Package::Name.'.sort_by.nulls'; + $contextFactory = static function (): Context { + return new Context(); + }; $getSorterFactory = static function (bool $nullsSortable): Closure { return static function () use ($nullsSortable): Sorter { return new class($nullsSortable) implements Sorter { @@ -242,6 +276,7 @@ public function sort( null, null, $getSorterFactory(true), + $contextFactory, Direction::Asc, ], 'nulls are not sortable' => [ @@ -250,6 +285,7 @@ public function sort( $key => Nulls::First, ], $getSorterFactory(false), + $contextFactory, Direction::Asc, ], 'nulls are sortable (asc)' => [ @@ -258,6 +294,7 @@ public function sort( $key => Nulls::Last, ], $getSorterFactory(true), + $contextFactory, Direction::Asc, ], 'nulls are sortable (desc)' => [ @@ -266,6 +303,7 @@ public function sort( $key => Nulls::Last, ], $getSorterFactory(true), + $contextFactory, Direction::Desc, ], 'nulls are sortable (separate)' => [ @@ -277,6 +315,7 @@ public function sort( ], ], $getSorterFactory(true), + $contextFactory, Direction::Desc, ], '(deprecated) nulls are sortable (asc)' => [ @@ -285,6 +324,7 @@ public function sort( $key => Nulls::Last, ], $getSorterFactory(true), + $contextFactory, Direction::asc, ], '(deprecated) nulls are sortable (desc)' => [ @@ -293,6 +333,7 @@ public function sort( $key => Nulls::Last, ], $getSorterFactory(true), + $contextFactory, Direction::desc, ], '(deprecated) nulls are sortable (separate)' => [ @@ -304,8 +345,35 @@ public function sort( ], ], $getSorterFactory(true), + $contextFactory, Direction::desc, ], + 'nulls are sortable (Context null)' => [ + null, + [ + $key => Nulls::Last, + ], + $getSorterFactory(true), + static function (): Context { + return (new Context())->override([ + FieldContextNulls::class => new FieldContextNulls(null), + ]); + }, + Direction::Desc, + ], + 'nulls are sortable (Context first)' => [ + Nulls::First, + [ + $key => Nulls::Last, + ], + $getSorterFactory(true), + static function (): Context { + return (new Context())->override([ + FieldContextNulls::class => new FieldContextNulls(Nulls::First), + ]); + }, + Direction::Desc, + ], ]; } // diff --git a/packages/graphql/src/Stream/Directives/DirectiveTest~expected.graphql b/packages/graphql/src/Stream/Directives/DirectiveTest~expected.graphql index 2f7f2da60..1b276ef22 100644 --- a/packages/graphql/src/Stream/Directives/DirectiveTest~expected.graphql +++ b/packages/graphql/src/Stream/Directives/DirectiveTest~expected.graphql @@ -48,6 +48,18 @@ on | INPUT_FIELD_DEFINITION | SCALAR +directive @sortByOperatorNullsFirst +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + +directive @sortByOperatorNullsLast +on + | ENUM + | INPUT_FIELD_DEFINITION + | SCALAR + """ Splits list of items into the chunks and returns one chunk specified by an offset or a cursor. @@ -191,6 +203,18 @@ input SortByClauseTestObject { """ id: SortByTypeDirection @sortByOperatorField + + """ + NULLs first + """ + nullsFirst: SortByClauseTestObject + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByClauseTestObject + @sortByOperatorNullsLast } """ @@ -202,6 +226,18 @@ input SortByScoutClauseTestObject { """ id: SortByTypeDirection @sortByOperatorField + + """ + NULLs first + """ + nullsFirst: SortByScoutClauseTestObject + @sortByOperatorNullsFirst + + """ + NULLs last + """ + nullsLast: SortByScoutClauseTestObject + @sortByOperatorNullsLast } """