diff --git a/packages/graphql/README.md b/packages/graphql/README.md index e673bb115..f538289ab 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -129,16 +129,16 @@ query { ``` -## Scalars +## Config -In addition to standard GraphQL scalars the package defines few own: +In addition to standard GraphQL types the package defines few own: -* `LastDragon_ru\\LaraASP\\GraphQL\\SearchBy\\Directives\\Directive::ScalarNumber` - any operator for this scalar will be available for `Int` and `Float`; -* `LastDragon_ru\\LaraASP\\GraphQL\\SearchBy\\Directives\\Directive::ScalarNull` - additional operators available for nullable scalars; -* `LastDragon_ru\\LaraASP\\GraphQL\\SearchBy\\Directives\\Directive::ScalarLogic` - list of logical operators, please see below; -* `LastDragon_ru\\LaraASP\\GraphQL\\SearchBy\\Directives\\Directive::ScalarEnum` - default operators for enums; +* `LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators::Number` - any operator for this type will be available for `Int` and `Float`; +* `LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators::Null` - additional operators available for nullable types; +* `LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators::Logical` - list of logical operators, please see below; +* `LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators::Enum` - default operators for enums; -To work with custom scalars you need to configure supported operators for each of them. First, you need to publish package config: +To work with custom types you need to configure supported operators for each of them. First, you need to publish package config: ```shell php artisan vendor:publish --provider=LastDragon_ru\\LaraASP\\GraphQL\\Provider --tag=config @@ -165,15 +165,15 @@ return [ */ 'search_by' => [ /** - * Scalars + * Operators * --------------------------------------------------------------------- * - * You can (re)define scalars and supported operators here. + * You can (re)define types and supported operators here. * - * @var array>> + * @var array>> */ - 'scalars' => [ - // You can define a list of operators for each Scalar + 'operators' => [ + // You can define a list of operators for each type 'Date' => [ Equal::class, Between::class, @@ -195,8 +195,8 @@ return [ There are three types of operators: -* Comparison - used to compare column with value(s), eg `{equal: "value"}`, `{lt: 2}`, etc. To add your own you just need to implement [`Operator`](./src/SearchBy/Contracts/Operator.php) and add it to scalar(s); -* Logical - used to group comparisons into groups, eg `anyOf([{equal: "a"}, {equal: "b"}])`. Adding your own is the same: implement [`Operator`](./src/SearchBy/Contracts/Operator.php) and add it to `Directive::ScalarLogic` scalar; +* Comparison - used to compare column with value(s), eg `{equal: "value"}`, `{lt: 2}`, etc. To add your own you just need to implement [`Operator`](./src/SearchBy/Contracts/Operator.php) and add it to type(s); +* Logical - used to group comparisons into groups, eg `anyOf([{equal: "a"}, {equal: "b"}])`. Adding your own is the same: implement [`Operator`](./src/SearchBy/Contracts/Operator.php) and add it to `Operators::Logical` type; * Complex - used to create conditions for nested Input types and allow implement any logic eg `whereHas`, `whereDoesntHave`, etc. These operators must implement [`ComplexOperator`](./src/SearchBy/Contracts/ComplexOperator.php) (by default the [`Relation`](./src/SearchBy/Operators/Complex/Relation.php) operator will be used, you can use it as example): ```graphql diff --git a/packages/graphql/config/config.php b/packages/graphql/config/config.php index 6706eeeee..db585cb45 100644 --- a/packages/graphql/config/config.php +++ b/packages/graphql/config/config.php @@ -1,6 +1,7 @@ [ /** - * Scalars + * Operators * --------------------------------------------------------------------- * - * You can (re)define scalars and supported operators here. + * You can (re)define types and supported operators here. * - * @var array>> + * @var array>> */ - 'scalars' => [ + 'operators' => [ // This value has no effect inside the published config. ConfigMerger::Replace => true, ], diff --git a/packages/graphql/src/Builder/Contracts/TypeDefinition.php b/packages/graphql/src/Builder/Contracts/TypeDefinition.php index badccdedf..b0030c99e 100644 --- a/packages/graphql/src/Builder/Contracts/TypeDefinition.php +++ b/packages/graphql/src/Builder/Contracts/TypeDefinition.php @@ -2,17 +2,18 @@ namespace LastDragon_ru\LaraASP\GraphQL\Builder\Contracts; +use GraphQL\Language\AST\Node; use GraphQL\Language\AST\TypeDefinitionNode; interface TypeDefinition { public static function getName(): string; /** - * @return (TypeDefinitionNode&\GraphQL\Language\AST\Node)|null + * @return (TypeDefinitionNode&Node)|null */ public function getTypeDefinitionNode( string $name, - string $scalar = null, + string $type = null, bool $nullable = null, ): ?TypeDefinitionNode; } diff --git a/packages/graphql/src/Builder/Contracts/TypeProvider.php b/packages/graphql/src/Builder/Contracts/TypeProvider.php index f0159e1bc..42fbe8d0f 100644 --- a/packages/graphql/src/Builder/Contracts/TypeProvider.php +++ b/packages/graphql/src/Builder/Contracts/TypeProvider.php @@ -4,7 +4,7 @@ interface TypeProvider { /** - * @param class-string $type + * @param class-string $definition */ - public function getType(string $type, string $scalar = null, bool $nullable = null): string; + public function getType(string $definition, string $type = null, bool $nullable = null): string; } diff --git a/packages/graphql/src/Builder/Exceptions/TypeDefinitionImpossibleToCreateType.php b/packages/graphql/src/Builder/Exceptions/TypeDefinitionImpossibleToCreateType.php index 9496c21a0..6848c029f 100644 --- a/packages/graphql/src/Builder/Exceptions/TypeDefinitionImpossibleToCreateType.php +++ b/packages/graphql/src/Builder/Exceptions/TypeDefinitionImpossibleToCreateType.php @@ -13,15 +13,15 @@ class TypeDefinitionImpossibleToCreateType extends BuilderException { */ public function __construct( protected string $definition, - protected ?string $scalar, + protected ?string $type, protected ?bool $nullable, Throwable $previous = null, ) { parent::__construct( sprintf( - 'Definition `%s`: Impossible to create type for scalar `%s`.', + 'Definition `%s`: Impossible to create type for type `%s`.', $this->definition, - ($this->scalar ?: 'null').($this->nullable ? '' : '!'), + ($this->type ?: 'null').($this->nullable ? '' : '!'), ), $previous, ); @@ -31,8 +31,8 @@ public function getDefinition(): string { return $this->definition; } - public function getScalar(): ?string { - return $this->scalar; + public function getType(): ?string { + return $this->type; } public function isNullable(): ?bool { diff --git a/packages/graphql/src/SearchBy/Exceptions/ScalarNoOperators.php b/packages/graphql/src/Builder/Exceptions/TypeNoOperators.php similarity index 66% rename from packages/graphql/src/SearchBy/Exceptions/ScalarNoOperators.php rename to packages/graphql/src/Builder/Exceptions/TypeNoOperators.php index f1c210007..379171e01 100644 --- a/packages/graphql/src/SearchBy/Exceptions/ScalarNoOperators.php +++ b/packages/graphql/src/Builder/Exceptions/TypeNoOperators.php @@ -1,18 +1,18 @@ name, ), $previous); } diff --git a/packages/graphql/src/SearchBy/Exceptions/ScalarUnknown.php b/packages/graphql/src/Builder/Exceptions/TypeUnknown.php similarity index 69% rename from packages/graphql/src/SearchBy/Exceptions/ScalarUnknown.php rename to packages/graphql/src/Builder/Exceptions/TypeUnknown.php index 6f1608d1b..701352eae 100644 --- a/packages/graphql/src/SearchBy/Exceptions/ScalarUnknown.php +++ b/packages/graphql/src/Builder/Exceptions/TypeUnknown.php @@ -1,18 +1,18 @@ name, ), $previous); } diff --git a/packages/graphql/src/Builder/Manipulator.php b/packages/graphql/src/Builder/Manipulator.php index 043990f0a..3f5a6c2c0 100644 --- a/packages/graphql/src/Builder/Manipulator.php +++ b/packages/graphql/src/Builder/Manipulator.php @@ -42,34 +42,34 @@ protected function getContainer(): Container { // // ========================================================================= - public function getType(string $type, string $scalar = null, bool $nullable = null): string { + public function getType(string $definition, string $type = null, bool $nullable = null): string { // Exists? - $name = $this->getTypeName($type::getName(), $scalar, $nullable); + $name = $this->getTypeName($definition::getName(), $type, $nullable); if ($this->isTypeDefinitionExists($name)) { return $name; } // Create new - $instance = $this->getContainer()->make($type); - $definition = $instance->getTypeDefinitionNode($name, $scalar, $nullable); + $instance = $this->getContainer()->make($definition); + $node = $instance->getTypeDefinitionNode($name, $type, $nullable); - if (!$definition) { - throw new TypeDefinitionImpossibleToCreateType($type, $scalar, $nullable); + if (!$node) { + throw new TypeDefinitionImpossibleToCreateType($definition, $type, $nullable); } - if ($name !== $this->getNodeName($definition)) { - throw new TypeDefinitionInvalidTypeName($type, $name, $this->getNodeName($definition)); + if ($name !== $this->getNodeName($node)) { + throw new TypeDefinitionInvalidTypeName($definition, $name, $this->getNodeName($node)); } // Save - $this->addTypeDefinition($definition); + $this->addTypeDefinition($node); // Return return $name; } - abstract protected function getTypeName(string $name, string $scalar = null, bool $nullable = null): string; + abstract protected function getTypeName(string $name, string $type = null, bool $nullable = null): string; // // diff --git a/packages/graphql/src/Builder/Operators.php b/packages/graphql/src/Builder/Operators.php new file mode 100644 index 000000000..6baa3c533 --- /dev/null +++ b/packages/graphql/src/Builder/Operators.php @@ -0,0 +1,117 @@ +>|string> + */ + protected array $operators = []; + + /** + * Determines additional operators available for type. + * + * @var array + */ + protected array $extends = []; + + public function __construct( + private Container $container, + ) { + // empty + } + + protected function getContainer(): Container { + return $this->container; + } + + public function hasOperators(string $type): bool { + return isset($this->operators[$type]); + } + + /** + * @param array>|string $operators + */ + public function addOperators(string $type, array|string $operators): void { + if (is_string($operators) && !$this->hasOperators($operators)) { + throw new TypeUnknown($operators); + } + + if (is_array($operators) && !$operators) { + throw new TypeNoOperators($type); + } + + $this->operators[$type] = $operators; + } + + /** + * @return array + */ + public function getOperators(string $type, bool $nullable): array { + // Is known? + if (!$this->hasOperators($type)) { + throw new TypeUnknown($type); + } + + // Base + $base = $type; + $operators = $type; + + do { + $operators = $this->operators[$operators] ?? []; + $isAlias = !is_array($operators); + + if ($isAlias) { + $base = $operators; + } + } while ($isAlias); + + // Create Instances + $container = $this->getContainer(); + $operators = array_map(static function (string $operator) use ($container): Operator { + return $container->make($operator); + }, array_unique($operators)); + + // Extends + if (isset($this->extends[$base])) { + $extends = $this->getOperators($this->extends[$base], $nullable); + $operators = array_merge($operators, $extends); + } + + // Add `null` for nullable + if ($nullable) { + array_push($operators, ...$this->getOperators(static::Null, false)); + } + + // Cleanup + $operators = array_values(array_unique($operators, SORT_REGULAR)); + + // Return + return $operators; + } +} diff --git a/packages/graphql/src/Builder/OperatorsTest.php b/packages/graphql/src/Builder/OperatorsTest.php new file mode 100644 index 000000000..767a05087 --- /dev/null +++ b/packages/graphql/src/Builder/OperatorsTest.php @@ -0,0 +1,279 @@ + + // ========================================================================= + /** + * @covers ::hasOperators + */ + public function testHasOperators(): void { + $operators = new class($this->app) extends Operators { + /** + * @inheritdoc + */ + protected array $operators = [ + Operators::Int => [ + OperatorsTest__OperatorA::class, + ], + ]; + }; + + self::assertTrue($operators->hasOperators(Operators::Int)); + self::assertFalse($operators->hasOperators('unknown')); + } + + /** + * @covers ::addOperators + * + * @dataProvider dataProviderAddOperators + */ + public function testAddOperators(Exception|bool $expected, string $type, mixed $typeOperators): void { + if ($expected instanceof Exception) { + self::expectExceptionObject($expected); + } + + $operators = new class($this->app) extends Operators { + // empty + }; + + $operators->addOperators($type, $typeOperators); + + self::assertEquals($expected, $operators->hasOperators($type)); + } + + /** + * @covers ::getOperators + */ + public function testGetOperators(): void { + $type = __FUNCTION__; + $alias = 'alias'; + $operators = new class($this->app) extends Operators { + // empty + }; + + $operators->addOperators($type, [ + OperatorsTest__OperatorA::class, + OperatorsTest__OperatorA::class, + ]); + $operators->addOperators($alias, $type); + $operators->addOperators(Operators::Null, [ + OperatorsTest__OperatorB::class, + OperatorsTest__OperatorC::class, + ]); + + self::assertEquals( + [OperatorsTest__OperatorA::class], + $this->toClassNames($operators->getOperators($type, false)), + ); + self::assertEquals( + [ + OperatorsTest__OperatorA::class, + OperatorsTest__OperatorB::class, + OperatorsTest__OperatorC::class, + ], + $this->toClassNames($operators->getOperators($type, true)), + ); + self::assertEquals( + $operators->getOperators($type, false), + $operators->getOperators($alias, false), + ); + self::assertEquals( + $operators->getOperators($type, true), + $operators->getOperators($alias, true), + ); + } + + /** + * @covers ::getOperators + */ + public function testGetOperatorsExtends(): void { + $operators = new class($this->app) extends Operators { + /** + * @inheritdoc + */ + protected array $extends = [ + 'test' => 'base', + ]; + }; + + $operators->addOperators('test', [ + OperatorsTest__OperatorA::class, + OperatorsTest__OperatorA::class, + ]); + $operators->addOperators('base', [ + OperatorsTest__OperatorD::class, + ]); + $operators->addOperators('alias', 'test'); + $operators->addOperators(Operators::Null, [ + OperatorsTest__OperatorB::class, + OperatorsTest__OperatorC::class, + ]); + + self::assertEquals( + [OperatorsTest__OperatorA::class, OperatorsTest__OperatorD::class], + $this->toClassNames($operators->getOperators('test', false)), + ); + self::assertEquals( + [ + OperatorsTest__OperatorA::class, + OperatorsTest__OperatorD::class, + OperatorsTest__OperatorB::class, + OperatorsTest__OperatorC::class, + ], + $this->toClassNames($operators->getOperators('test', true)), + ); + self::assertEquals( + [ + OperatorsTest__OperatorA::class, + OperatorsTest__OperatorD::class, + OperatorsTest__OperatorB::class, + OperatorsTest__OperatorC::class, + ], + $this->toClassNames($operators->getOperators('alias', true)), + ); + } + + /** + * @covers ::getOperators + */ + public function testGetOperatorsUnknownType(): void { + self::expectExceptionObject(new TypeUnknown('unknown')); + + $operators = new class($this->app) extends Operators { + // empty + }; + + $operators->getOperators('unknown', false); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public function dataProviderAddOperators(): array { + return [ + 'ok' => [true, 'scalar', [IsNot::class]], + 'unknown scalar' => [ + new TypeUnknown('unknown'), + 'scalar', + 'unknown', + ], + 'empty operators' => [ + new TypeNoOperators('scalar'), + 'scalar', + [], + ], + ]; + } + // + + // + // ========================================================================= + /** + * @param array $objects + * + * @return array + */ + protected function toClassNames(array $objects): array { + $classes = []; + + foreach ($objects as $object) { + $classes[] = $object::class; + } + + return $classes; + } + // +} + +// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses +// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +abstract class OperatorsTest__Operator implements Operator { + public static function definition(): string { + throw new Exception('Should not be called'); + } + + public static function getName(): string { + throw new Exception('Should not be called'); + } + + public static function getDirectiveName(): string { + throw new Exception('Should not be called'); + } + + public function getFieldType(TypeProvider $provider, string $type): ?string { + throw new Exception('Should not be called'); + } + + public function getFieldDescription(): string { + throw new Exception('Should not be called'); + } + + public function getFieldDirective(): ?DirectiveNode { + throw new Exception('Should not be called'); + } + + public function isBuilderSupported(object $builder): bool { + throw new Exception('Should not be called'); + } + + public function call(Handler $handler, object $builder, Property $property, Argument $argument): object { + throw new Exception('Should not be called'); + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class OperatorsTest__OperatorA extends OperatorsTest__Operator { + // empty +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class OperatorsTest__OperatorB extends OperatorsTest__Operator { + // empty +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class OperatorsTest__OperatorC extends OperatorsTest__Operator { + // empty +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class OperatorsTest__OperatorD extends OperatorsTest__Operator { + // empty +} diff --git a/packages/graphql/src/Builder/Traits/WithOperators.php b/packages/graphql/src/Builder/Traits/WithOperators.php new file mode 100644 index 000000000..d6391883e --- /dev/null +++ b/packages/graphql/src/Builder/Traits/WithOperators.php @@ -0,0 +1,24 @@ + + */ + protected function getTypeOperators(string $type, bool $nullable): array { + $operators = $this->getOperators()->getOperators($type, $nullable); + + if (!$operators) { + throw new TypeNoOperators($type); + } + + return $operators; + } +} diff --git a/packages/graphql/src/Provider.php b/packages/graphql/src/Provider.php index bc1703c56..5c13fd122 100644 --- a/packages/graphql/src/Provider.php +++ b/packages/graphql/src/Provider.php @@ -13,7 +13,7 @@ use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\SchemaPrinter; use LastDragon_ru\LaraASP\GraphQL\SchemaPrinter\Settings\DefaultSettings; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByDirective; -use LastDragon_ru\LaraASP\GraphQL\SearchBy\Scalars; +use LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators; use LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByDirective; use LastDragon_ru\LaraASP\GraphQL\Utils\Enum\EnumType; use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces; @@ -57,7 +57,7 @@ static function (): string { } protected function registerSearchByDirective(): void { - $this->app->singleton(Scalars::class); + $this->app->singleton(Operators::class); } protected function registerEnums(): void { diff --git a/packages/graphql/src/SearchBy/Directives/Directive.php b/packages/graphql/src/SearchBy/Directives/Directive.php index 53b2f9713..41f167e1e 100644 --- a/packages/graphql/src/SearchBy/Directives/Directive.php +++ b/packages/graphql/src/SearchBy/Directives/Directive.php @@ -15,16 +15,7 @@ use Nuwave\Lighthouse\Support\Contracts\ArgManipulator; class Directive extends HandlerDirective implements ArgManipulator, ArgBuilderDirective { - public const Name = 'SearchBy'; - public const ScalarID = 'ID'; - public const ScalarInt = 'Int'; - public const ScalarFloat = 'Float'; - public const ScalarString = 'String'; - public const ScalarBoolean = 'Boolean'; - public const ScalarEnum = self::Name.'Enum'; - public const ScalarNull = self::Name.'Null'; - public const ScalarLogic = self::Name.'Logic'; - public const ScalarNumber = self::Name.'Number'; + public const Name = 'SearchBy'; public static function definition(): string { return /** @lang GraphQL */ <<<'GRAPHQL' diff --git a/packages/graphql/src/SearchBy/Directives/DirectiveTest.php b/packages/graphql/src/SearchBy/Directives/DirectiveTest.php index d93de1669..502d8e44f 100644 --- a/packages/graphql/src/SearchBy/Directives/DirectiveTest.php +++ b/packages/graphql/src/SearchBy/Directives/DirectiveTest.php @@ -273,7 +273,7 @@ static function (TestCase $test): void { $package = Package::Name; $config = $test->app->make(Repository::class); - $config->set("{$package}.search_by.scalars.Date", [ + $config->set("{$package}.search_by.operators.Date", [ Between::class, ]); }, diff --git a/packages/graphql/src/SearchBy/Manipulator.php b/packages/graphql/src/SearchBy/Manipulator.php index 813d44fad..68da3af6f 100644 --- a/packages/graphql/src/SearchBy/Manipulator.php +++ b/packages/graphql/src/SearchBy/Manipulator.php @@ -18,8 +18,8 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Support\Str; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator as OperatorContract; -use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeProvider; use LastDragon_ru\LaraASP\GraphQL\Builder\Manipulator as BuilderManipulator; +use LastDragon_ru\LaraASP\GraphQL\Builder\Traits\WithOperators; use LastDragon_ru\LaraASP\GraphQL\Exceptions\TypeDefinitionUnknown; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Contracts\ComplexOperator; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Directives\Directive; @@ -30,7 +30,6 @@ use LastDragon_ru\LaraASP\GraphQL\SearchBy\Exceptions\FakeTypeDefinitionUnknown; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Exceptions\InputFieldAlreadyDefined; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Exceptions\NotImplemented; -use LastDragon_ru\LaraASP\GraphQL\SearchBy\Exceptions\ScalarNoOperators; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators\Complex\Relation; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators\Property; use Nuwave\Lighthouse\Schema\AST\DocumentAST; @@ -42,21 +41,23 @@ use function is_string; use function str_starts_with; -class Manipulator extends BuilderManipulator implements TypeProvider { +class Manipulator extends BuilderManipulator { + use WithOperators; + public function __construct( Container $container, DirectiveLocator $directives, DocumentAST $document, TypeRegistry $types, - private Scalars $scalars, + private Operators $operators, ) { parent::__construct($container, $directives, $document, $types); } // // ========================================================================= - protected function getScalars(): Scalars { - return $this->scalars; + protected function getOperators(): Operators { + return $this->operators; } // @@ -97,10 +98,10 @@ public function getInputType(InputObjectTypeDefinitionNode|InputObjectType $node } // Add type - $operators = $this->getScalarOperators(Directive::ScalarLogic, false); - $scalar = $this->getScalarTypeNode($name); - $content = $this->getOperatorsFields($operators, $scalar); - $type = $this->addTypeDefinition( + $logical = $this->getTypeOperators(Operators::Logical, false); + $scalar = $this->getScalarTypeNode($name); + $content = $this->getOperatorsFields($logical, $scalar); + $type = $this->addTypeDefinition( Parser::inputObjectTypeDefinition( <<getContainer()->make(Property::class); - $scalars = $this->getScalars(); - $fields = $node instanceof InputObjectType + $operators = $this->getOperators(); + $property = $this->getContainer()->make(Property::class); + $fields = $node instanceof InputObjectType ? $node->getFields() : $node->fields; @@ -141,7 +142,7 @@ public function getInputType(InputObjectTypeDefinitionNode|InputObjectType $node try { $fieldTypeNode = $this->getTypeDefinitionNode($field); } catch (TypeDefinitionUnknown $exception) { - if ($scalars->isScalar($fieldType)) { + if ($operators->hasOperators($fieldType)) { $fieldTypeNode = $this->getScalarTypeNode($fieldType); } else { throw $exception; @@ -229,7 +230,7 @@ public function getScalarType(ScalarTypeDefinitionNode|ScalarType $type, bool $n // Determine supported operators $scalar = $this->getNodeName($type); - $operators = $this->getScalarOperators($scalar, $nullable); + $operators = $this->getTypeOperators($scalar, $nullable); // Add type $mark = $nullable ? '' : '!'; @@ -291,8 +292,8 @@ protected function getComplexType( // // ========================================================================= - protected function getTypeName(string $name, string $scalar = null, bool $nullable = null): string { - return Directive::Name.'Type'.Str::studly($name).($scalar ?: '').($nullable ? 'OrNull' : ''); + protected function getTypeName(string $name, string $type = null, bool $nullable = null): string { + return Directive::Name.'Type'.Str::studly($name).($type ?: '').($nullable ? 'OrNull' : ''); } protected function getConditionTypeName(InputObjectTypeDefinitionNode|InputObjectType $node): string { @@ -324,7 +325,10 @@ protected function getComplexTypeName( * @return array */ protected function getEnumOperators(string $enum, bool $nullable): array { - $operators = $this->getScalars()->getEnumOperators($enum, $nullable); + $operators = $this->getOperators(); + $operators = $operators->hasOperators($enum) + ? $operators->getOperators($enum, $nullable) + : $operators->getOperators(Operators::Enum, $nullable); if (!$operators) { throw new EnumNoOperators($enum); @@ -333,19 +337,6 @@ protected function getEnumOperators(string $enum, bool $nullable): array { return $operators; } - /** - * @return array - */ - protected function getScalarOperators(string $scalar, bool $nullable): array { - $operators = $this->getScalars()->getScalarOperators($scalar, $nullable); - - if (!$operators) { - throw new ScalarNoOperators($scalar); - } - - return $operators; - } - protected function getComplexOperator( InputValueDefinitionNode|InputObjectTypeDefinitionNode|InputObjectField|InputObjectType ...$nodes, ): ComplexOperator { diff --git a/packages/graphql/src/SearchBy/Operators.php b/packages/graphql/src/SearchBy/Operators.php new file mode 100644 index 000000000..a149bef35 --- /dev/null +++ b/packages/graphql/src/SearchBy/Operators.php @@ -0,0 +1,126 @@ + [ + Equal::class, + NotEqual::class, + In::class, + NotIn::class, + ], + Operators::Int => [ + BitwiseOr::class, + BitwiseXor::class, + BitwiseAnd::class, + BitwiseLeftShift::class, + BitwiseRightShift::class, + ], + Operators::Float => Operators::Number, + Operators::Boolean => [ + Equal::class, + ], + Operators::String => [ + Equal::class, + NotEqual::class, + Like::class, + NotLike::class, + In::class, + NotIn::class, + Contains::class, + StartsWith::class, + EndsWith::class, + ], + + // Special types + Operators::Number => [ + Equal::class, + NotEqual::class, + LessThan::class, + LessThanOrEqual::class, + GreaterThan::class, + GreaterThanOrEqual::class, + In::class, + NotIn::class, + Between::class, + NotBetween::class, + ], + Operators::Enum => [ + Equal::class, + NotEqual::class, + In::class, + NotIn::class, + ], + Operators::Null => [ + IsNull::class, + IsNotNull::class, + ], + Operators::Logical => [ + AllOf::class, + AnyOf::class, + Not::class, + ], + ]; + + /** + * @inheritdoc + */ + protected array $extends = [ + Operators::Int => Operators::Number, + Operators::Float => Operators::Number, + ]; + + public function __construct( + Container $container, + Repository $config, + ) { + parent::__construct($container); + + /** @var array>|string> $operators */ + $operators = (array) $config->get(Package::Name.'.search_by.operators'); + + foreach ($operators as $type => $typeOperators) { + $this->addOperators($type, $typeOperators); + } + } +} diff --git a/packages/graphql/src/SearchBy/Operators/Complex/Relation.php b/packages/graphql/src/SearchBy/Operators/Complex/Relation.php index 875cea687..ee8d7ea54 100644 --- a/packages/graphql/src/SearchBy/Operators/Complex/Relation.php +++ b/packages/graphql/src/SearchBy/Operators/Complex/Relation.php @@ -8,13 +8,13 @@ use GraphQL\Language\Parser; use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\Type; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use LastDragon_ru\LaraASP\Eloquent\ModelHelper; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Handler; use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\OperatorUnsupportedBuilder; use LastDragon_ru\LaraASP\GraphQL\Builder\Property; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Contracts\ComplexOperator; -use LastDragon_ru\LaraASP\GraphQL\SearchBy\Directives\Directive; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Exceptions\OperatorInvalidArgumentValue; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Manipulator; use LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators\BaseOperator; @@ -46,7 +46,7 @@ public function getDefinition( string $name, bool $nullable, ): InputObjectTypeDefinitionNode { - $count = $ast->getScalarType($ast->getScalarTypeNode(Directive::ScalarInt), false); + $count = $ast->getScalarType($ast->getScalarTypeNode(Type::INT), false); $where = $ast->getInputType($type); return Parser::inputObjectTypeDefinition( diff --git a/packages/graphql/src/SearchBy/OperatorsTest.php b/packages/graphql/src/SearchBy/OperatorsTest.php new file mode 100644 index 000000000..12bbb1eba --- /dev/null +++ b/packages/graphql/src/SearchBy/OperatorsTest.php @@ -0,0 +1,71 @@ + + // ========================================================================= + /** + * @covers ::__construct + */ + public function testConstructor(): void { + $config = Mockery::mock(Repository::class); + $config + ->shouldReceive('get') + ->with(Package::Name.'.search_by.operators') + ->andReturn([ + Operators::ID => [ + Equal::class, + ], + Operators::Int => [ + NotEqual::class, + ], + ]); + + $operators = new class($this->app, $config) extends Operators { + // empty + }; + + self::assertTrue($operators->hasOperators(Operators::ID)); + self::assertTrue($operators->hasOperators(Operators::Int)); + self::assertFalse($operators->hasOperators('unknown')); + self::assertEquals( + [ + Equal::class, + ], + $this->toClassNames( + $operators->getOperators(Operators::ID, false), + ), + ); + } + // + + // + // ========================================================================= + /** + * @param array $objects + * + * @return array + */ + protected function toClassNames(array $objects): array { + $classes = []; + + foreach ($objects as $object) { + $classes[] = $object::class; + } + + return $classes; + } + // +} diff --git a/packages/graphql/src/SearchBy/Scalars.php b/packages/graphql/src/SearchBy/Scalars.php deleted file mode 100644 index 3f0aac72f..000000000 --- a/packages/graphql/src/SearchBy/Scalars.php +++ /dev/null @@ -1,215 +0,0 @@ ->|string> - */ - protected array $scalars = [ - // Standard types - Directive::ScalarID => [ - Equal::class, - NotEqual::class, - In::class, - NotIn::class, - ], - Directive::ScalarInt => [ - BitwiseOr::class, - BitwiseXor::class, - BitwiseAnd::class, - BitwiseLeftShift::class, - BitwiseRightShift::class, - ], - Directive::ScalarFloat => Directive::ScalarNumber, - Directive::ScalarBoolean => [ - Equal::class, - ], - Directive::ScalarString => [ - Equal::class, - NotEqual::class, - Like::class, - NotLike::class, - In::class, - NotIn::class, - Contains::class, - StartsWith::class, - EndsWith::class, - ], - - // Special types - Directive::ScalarNumber => [ - Equal::class, - NotEqual::class, - LessThan::class, - LessThanOrEqual::class, - GreaterThan::class, - GreaterThanOrEqual::class, - In::class, - NotIn::class, - Between::class, - NotBetween::class, - ], - Directive::ScalarEnum => [ - Equal::class, - NotEqual::class, - In::class, - NotIn::class, - ], - Directive::ScalarNull => [ - IsNull::class, - IsNotNull::class, - ], - Directive::ScalarLogic => [ - AllOf::class, - AnyOf::class, - Not::class, - ], - ]; - - /** - * Determines additional operators available for scalar type. - * - * @var array - */ - protected array $extends = [ - Directive::ScalarInt => Directive::ScalarNumber, - Directive::ScalarFloat => Directive::ScalarNumber, - ]; - - public function __construct( - private Container $container, - Repository $config, - ) { - /** @var array>|string> $scalars */ - $scalars = (array) $config->get(Package::Name.'.search_by.scalars'); - - foreach ($scalars as $scalar => $operators) { - $this->addScalar($scalar, $operators); - } - } - - protected function getContainer(): Container { - return $this->container; - } - - public function isScalar(string $scalar): bool { - return isset($this->scalars[$scalar]); - } - - /** - * @param array>|string $operators - */ - public function addScalar(string $scalar, array|string $operators): void { - if (is_string($operators) && !$this->isScalar($operators)) { - throw new ScalarUnknown($operators); - } - - if (is_array($operators) && !$operators) { - throw new ScalarNoOperators($scalar); - } - - $this->scalars[$scalar] = $operators; - } - - /** - * @return array - */ - public function getScalarOperators(string $scalar, bool $nullable): array { - // Is Scalar? - if (!$this->isScalar($scalar)) { - throw new ScalarUnknown($scalar); - } - - // Base - $base = $scalar; - $operators = $scalar; - - do { - $operators = $this->scalars[$operators] ?? []; - $isAlias = !is_array($operators); - - if ($isAlias) { - $base = $operators; - } - } while ($isAlias); - - // Create Instances - $container = $this->getContainer(); - $operators = array_map(static function (string $operator) use ($container): OperatorContract { - return $container->make($operator); - }, array_unique($operators)); - - // Extends - if (isset($this->extends[$base])) { - $extends = $this->getScalarOperators($this->extends[$base], $nullable); - $operators = array_merge($operators, $extends); - } - - // Add `null` for nullable - if ($nullable) { - array_push($operators, ...$this->getScalarOperators(Directive::ScalarNull, false)); - } - - // Cleanup - $operators = array_values(array_unique($operators, SORT_REGULAR)); - - // Return - return $operators; - } - - /** - * @return array - */ - public function getEnumOperators(string $enum, bool $nullable): array { - return $this->isScalar($enum) - ? $this->getScalarOperators($enum, $nullable) - : $this->getScalarOperators(Directive::ScalarEnum, $nullable); - } -} diff --git a/packages/graphql/src/SearchBy/ScalarsTest.php b/packages/graphql/src/SearchBy/ScalarsTest.php deleted file mode 100644 index c4c0e15e3..000000000 --- a/packages/graphql/src/SearchBy/ScalarsTest.php +++ /dev/null @@ -1,216 +0,0 @@ - - // ========================================================================= - /** - * @before - */ - public function init(): void { - $this->afterApplicationCreated(function (): void { - $this->override(Repository::class, static function (MockInterface $mock): void { - $mock - ->shouldReceive('get') - ->andReturn(null); - }); - }); - } - // - - // - // ========================================================================= - /** - * @covers ::isScalar - */ - public function testIsScalar(): void { - $scalars = $this->app->make(Scalars::class); - - self::assertTrue($scalars->isScalar(Directive::ScalarInt)); - self::assertFalse($scalars->isScalar('unknown')); - } - - /** - * @covers ::addScalar - * - * @dataProvider dataProviderAddScalar - */ - public function testAddScalar(Exception|bool $expected, string $scalar, mixed $operators): void { - if ($expected instanceof Exception) { - self::expectExceptionObject($expected); - } - - $scalars = $this->app->make(Scalars::class); - - $scalars->addScalar($scalar, $operators); - - self::assertEquals($expected, $scalars->isScalar($scalar)); - } - - /** - * @covers ::getScalarOperators - */ - public function testGetScalarOperators(): void { - $scalar = __FUNCTION__; - $alias = 'alias'; - $scalars = $this->app->make(Scalars::class); - - $scalars->addScalar($scalar, [Equal::class, Equal::class]); - $scalars->addScalar($alias, $scalar); - - self::assertEquals( - [Equal::class], - $this->toClassNames($scalars->getScalarOperators($scalar, false)), - ); - self::assertEquals( - [Equal::class, IsNull::class, IsNotNull::class], - $this->toClassNames($scalars->getScalarOperators($scalar, true)), - ); - self::assertEquals( - $scalars->getScalarOperators($scalar, false), - $scalars->getScalarOperators($alias, false), - ); - self::assertEquals( - $scalars->getScalarOperators($scalar, true), - $scalars->getScalarOperators($alias, true), - ); - } - - /** - * @covers ::getScalarOperators - */ - public function testGetScalarOperatorsExtends(): void { - $config = $this->app->make(Repository::class); - $scalars = new class($this->app, $config) extends Scalars { - /** - * @var array - */ - protected array $extends = [ - 'test' => 'base', - ]; - }; - - $scalars->addScalar('test', [Equal::class, Equal::class]); - $scalars->addScalar('base', [NotEqual::class]); - $scalars->addScalar('alias', 'test'); - - self::assertEquals( - [Equal::class, NotEqual::class], - $this->toClassNames($scalars->getScalarOperators('test', false)), - ); - self::assertEquals( - [Equal::class, NotEqual::class, IsNull::class, IsNotNull::class], - $this->toClassNames($scalars->getScalarOperators('test', true)), - ); - self::assertEquals( - [Equal::class, NotEqual::class, IsNull::class, IsNotNull::class], - $this->toClassNames($scalars->getScalarOperators('alias', true)), - ); - } - - /** - * @covers ::getScalarOperators - */ - public function testGetScalarOperatorsUnknownScalar(): void { - self::expectExceptionObject(new ScalarUnknown('unknown')); - - $this->app->make(Scalars::class)->getScalarOperators('unknown', false); - } - - /** - * @covers ::getEnumOperators - */ - public function testGetEnumOperators(): void { - $enum = __FUNCTION__; - $alias = 'alias'; - $scalars = $this->app->make(Scalars::class); - - $scalars->addScalar($enum, [Equal::class, Equal::class]); - $scalars->addScalar($alias, $enum); - $scalars->addScalar(Directive::ScalarEnum, [NotEqual::class, NotEqual::class]); - - self::assertEquals( - [NotEqual::class], - $this->toClassNames($scalars->getEnumOperators('unknown', false)), - ); - self::assertEquals( - [NotEqual::class, IsNull::class, IsNotNull::class], - $this->toClassNames($scalars->getEnumOperators('unknown', true)), - ); - self::assertEquals( - [Equal::class], - $this->toClassNames($scalars->getEnumOperators($enum, false)), - ); - self::assertEquals( - [Equal::class, IsNull::class, IsNotNull::class], - $this->toClassNames($scalars->getEnumOperators($enum, true)), - ); - self::assertEquals( - $scalars->getEnumOperators($enum, false), - $scalars->getEnumOperators($alias, false), - ); - self::assertEquals( - $scalars->getEnumOperators($enum, true), - $scalars->getEnumOperators($alias, true), - ); - } - // - - // - // ========================================================================= - /** - * @return array - */ - public function dataProviderAddScalar(): array { - return [ - 'ok' => [true, 'scalar', [IsNot::class]], - 'unknown scalar' => [ - new ScalarUnknown('unknown'), - 'scalar', - 'unknown', - ], - 'empty operators' => [ - new ScalarNoOperators('scalar'), - 'scalar', - [], - ], - ]; - } - // - - // - // ========================================================================= - /** - * @param array $objects - * - * @return array - */ - protected function toClassNames(array $objects): array { - $classes = []; - - foreach ($objects as $object) { - $classes[] = $object::class; - } - - return $classes; - } - // -} diff --git a/packages/graphql/src/SearchBy/Types/Flag.php b/packages/graphql/src/SearchBy/Types/Flag.php index b95502453..d2a939634 100644 --- a/packages/graphql/src/SearchBy/Types/Flag.php +++ b/packages/graphql/src/SearchBy/Types/Flag.php @@ -6,8 +6,6 @@ use GraphQL\Language\Parser; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeDefinition; -use function is_null; - class Flag implements TypeDefinition { public function __construct() { // empty @@ -19,13 +17,13 @@ public static function getName(): string { public function getTypeDefinitionNode( string $name, - string $scalar = null, + string $type = null, bool $nullable = null, ): ?TypeDefinitionNode { - $type = null; + $node = null; - if (is_null($scalar) && is_null($nullable)) { - $type = Parser::enumTypeDefinition( + if ($type === null && $nullable === null) { + $node = Parser::enumTypeDefinition( /** @lang GraphQL */ <<getNodeTypeName($node), Directive::Name); } - protected function getTypeName(string $name, string $scalar = null, bool $nullable = null): string { + protected function getTypeName(string $name, string $type = null, bool $nullable = null): string { return Directive::Name.'Type'.Str::studly($name); } diff --git a/packages/graphql/src/SortBy/Types/Direction.php b/packages/graphql/src/SortBy/Types/Direction.php index 4b378b519..0cfd6f443 100644 --- a/packages/graphql/src/SortBy/Types/Direction.php +++ b/packages/graphql/src/SortBy/Types/Direction.php @@ -6,8 +6,6 @@ use GraphQL\Language\Parser; use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeDefinition; -use function is_null; - class Direction implements TypeDefinition { public function __construct() { // empty @@ -19,13 +17,13 @@ public static function getName(): string { public function getTypeDefinitionNode( string $name, - string $scalar = null, + string $type = null, bool $nullable = null, ): ?TypeDefinitionNode { - $type = null; + $node = null; - if (is_null($scalar) && is_null($nullable)) { - $type = Parser::enumTypeDefinition( + if ($type === null && $nullable === null) { + $node = Parser::enumTypeDefinition( /** @lang GraphQL */ <<\\>\\|string, mixed given\\.$#" + count: 1 + path: packages/graphql/src/Builder/OperatorsTest.php + - message: "#^Property LastDragon_ru\\\\LaraASP\\\\GraphQL\\\\Builder\\\\Property\\:\\:\\$path \\(array\\\\) does not accept array\\\\.$#" count: 1 @@ -60,11 +65,6 @@ parameters: count: 1 path: packages/graphql/src/SearchBy/Directives/DirectiveTest.php - - - message: "#^Parameter \\#2 \\$operators of method LastDragon_ru\\\\LaraASP\\\\GraphQL\\\\SearchBy\\\\Scalars\\:\\:addScalar\\(\\) expects array\\\\>\\|string, mixed given\\.$#" - count: 1 - path: packages/graphql/src/SearchBy/ScalarsTest.php - - message: "#^Parameter \\#2 \\$callback of method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\\\:\\:when\\(\\) expects \\(callable\\(Illuminate\\\\Database\\\\Eloquent\\\\Builder\\, string\\|null\\)\\: Illuminate\\\\Database\\\\Eloquent\\\\Builder\\\\)\\|null, Closure\\(Illuminate\\\\Database\\\\Eloquent\\\\Builder, string\\)\\: Illuminate\\\\Database\\\\Eloquent\\\\Builder\\ given\\.$#" count: 1 @@ -214,4 +214,3 @@ parameters: message: "#^Parameter \\#2 \\$bindings of class LastDragon_ru\\\\LaraASP\\\\Testing\\\\Database\\\\QueryLog\\\\Query constructor expects array, mixed given\\.$#" count: 1 path: packages/testing/src/Utils/Args.php -