Skip to content

Commit

Permalink
feat(grahpql)!: @sortBy updated to use operator-directives (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
LastDragon-ru committed Jul 2, 2022
2 parents 4ac5ed4 + dd669b6 commit 744b313
Show file tree
Hide file tree
Showing 118 changed files with 1,876 additions and 1,908 deletions.
70 changes: 3 additions & 67 deletions packages/graphql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ The main feature - the ability to sort results by relation properties, at the mo
- `HasOneThrough` (https://laravel.com/docs/eloquent-relationships#has-one-through)


How to use:
How to use (and [generated GraphQL schema](./src/SortBy/Directives/DirectiveTest~example-expected.graphql)):

```graphql
type Query {
Expand Down Expand Up @@ -273,70 +273,6 @@ query {
}
```

<details>
<summary>Generated GraphQL schema</summary>

```graphql
type Comment {
text: String
user: User
}

type Query {
"""You can use normal input type"""
users(order: [SortByClauseUsersSort!]): ID!

"""or `_` to generate type automatically 😛"""
comments(order: [SortByClauseComment!]): [Comment!]!
}

"""Sort clause for type Comment (only one property allowed at a time)."""
input SortByClauseComment {
"""Property clause."""
text: SortByDirection

"""Property clause."""
user: SortByClauseUser
}

"""Sort clause for type User (only one property allowed at a time)."""
input SortByClauseUser {
"""Property clause."""
id: SortByDirection

"""Property clause."""
name: SortByDirection
}

"""
Sort clause for input UsersSort (only one property allowed at a time).
"""
input SortByClauseUsersSort {
"""Property clause."""
id: SortByDirection

"""Property clause."""
name: SortByDirection
}

"""Sort direction."""
enum SortByDirection {
asc
desc
}

type User {
id: ID!
name: String!
}

input UsersSort {
id: ID!
name: String!
}
```
</details>


## Scout

Expand All @@ -358,8 +294,8 @@ As you can see in the example above you can use the special placeholder `_` inst

- with list/array type
- with `@field` directive
- with `@sortByUnsortable` directive
- with any directive that implements [`Unsortable`](./src/SortBy/Contracts/Unsortable.php)
- with `@sortByIgnored` directive
- with any directive that implements [`Ignored`](./src/SortBy/Contracts/Ignored.php)


# Relations
Expand Down
17 changes: 17 additions & 0 deletions packages/graphql/src/Builder/Contracts/Handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Contracts;

use LastDragon_ru\LaraASP\GraphQL\Builder\Property;
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;

interface Handler {
/**
* @template TBuilder of object
*
* @param TBuilder $builder
*
* @return TBuilder
*/
public function handle(object $builder, Property $property, ArgumentSet $conditions): object;
}
40 changes: 40 additions & 0 deletions packages/graphql/src/Builder/Contracts/Operator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Contracts;

use GraphQL\Language\AST\DirectiveNode;
use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\OperatorUnsupportedBuilder;
use LastDragon_ru\LaraASP\GraphQL\Builder\Property;
use Nuwave\Lighthouse\Execution\Arguments\Argument;
use Nuwave\Lighthouse\Support\Contracts\Directive;

interface Operator extends Directive {
/**
* Must be a valid GraphQL Object Field name.
*/
public static function getName(): string;

/**
* Must start with `@` and be a valid GraphQL Directive name.
*/
public static function getDirectiveName(): string;

public function getFieldType(TypeProvider $provider, string $type): ?string;

public function getFieldDescription(): string;

public function getFieldDirective(): ?DirectiveNode;

public function isBuilderSupported(object $builder): bool;

/**
* @template TBuilder of object
*
* @param TBuilder $builder
*
* @throws OperatorUnsupportedBuilder if `$builder` is not supported
*
* @return TBuilder
*/
public function call(Handler $handler, object $builder, Property $property, Argument $argument): object;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\SearchBy\Contracts;
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&Node)|null
* @return (TypeDefinitionNode&\GraphQL\Language\AST\Node)|null
*/
public function getTypeDefinitionNode(
string $name,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\SearchBy\Contracts;
namespace LastDragon_ru\LaraASP\GraphQL\Builder\Contracts;

interface TypeProvider {
/**
Expand Down
143 changes: 143 additions & 0 deletions packages/graphql/src/Builder/Directives/HandlerDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Directives;

use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\ListTypeNode;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Collection;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Handler;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator;
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\Client\ConditionTooManyProperties;
use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\HandlerInvalidConditions;
use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\OperatorUnsupportedBuilder;
use LastDragon_ru\LaraASP\GraphQL\Builder\Property;
use LastDragon_ru\LaraASP\GraphQL\Utils\ArgumentFactory;
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Utils;

use function array_keys;
use function count;
use function is_array;

abstract class HandlerDirective extends BaseDirective implements Handler {
public function __construct(
private Container $container,
private ArgumentFactory $factory,
) {
// empty
}

protected function getContainer(): Container {
return $this->container;
}

protected function getFactory(): ArgumentFactory {
return $this->factory;
}

/**
* @template T of object
*
* @param T $builder
*
* @return T
*/
protected function handleAnyBuilder(object $builder, mixed $value): object {
if ($value !== null) {
$argument = $this->getFactory()->getArgument($this->definitionNode, $value);
$isList = $this->definitionNode instanceof InputValueDefinitionNode
&& $this->definitionNode->type instanceof ListTypeNode;
$conditions = $isList && is_array($argument->value)
? $argument->value
: [$argument->value];

foreach ($conditions as $condition) {
if ($condition instanceof ArgumentSet) {
$builder = $this->handle($builder, new Property(), $condition);
} else {
throw new HandlerInvalidConditions($this);
}
}
}

return $builder;
}

/**
* @template T of object
*
* @param T $builder
*
* @return T
*/
public function handle(object $builder, Property $property, ArgumentSet $conditions): object {
// Empty?
if (count($conditions->arguments) === 0) {
return $builder;
}

// Valid?
if (count($conditions->arguments) !== 1) {
throw new ConditionTooManyProperties(array_keys($conditions->arguments));
}

// Call
return $this->call($builder, $property, $conditions);
}

/**
* @template T of object
*
* @param T $builder
*
* @return T
*/
protected function call(object $builder, Property $property, ArgumentSet $operator): object {
// Operator & Value
/** @var Operator|null $op */
$op = null;
$value = null;
$filter = Utils::instanceofMatcher(Operator::class);

if (count($operator->arguments) > 1) {
throw new ConditionTooManyOperators(
array_keys($operator->arguments),
);
}

foreach ($operator->arguments as $name => $argument) {
/** @var Collection<int, Operator> $operators */
$operators = $argument->directives->filter($filter);
$property = $property->getChild($name);
$value = $argument;
$op = $operators->first();

if (count($operators) > 1) {
throw new ConditionTooManyOperators(
$operators
->map(static function (Operator $operator): string {
return $operator::getName();
})
->all(),
);
}
}

// Operator?
if (!$op || !$value) {
throw new ConditionEmpty();
}

// Supported?
if (!$op->isBuilderSupported($builder)) {
throw new OperatorUnsupportedBuilder($op, $builder);
}

// Return
return $op->call($this, $builder, $property, $value);
}
}
25 changes: 25 additions & 0 deletions packages/graphql/src/Builder/Directives/OperatorDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Directives;

use GraphQL\Language\AST\DirectiveNode;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Operator;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;

abstract class OperatorDirective extends BaseDirective implements Operator {
public function __construct() {
// empty
}

public static function definition(): string {
$name = static::getDirectiveName();

return /** @lang GraphQL */ <<<GRAPHQL
directive ${name} on INPUT_FIELD_DEFINITION
GRAPHQL;
}

public function getFieldDirective(): ?DirectiveNode {
return $this->directiveNode;
}
}
50 changes: 50 additions & 0 deletions packages/graphql/src/Builder/Directives/PropertyDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Directives;

use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\Handler;
use LastDragon_ru\LaraASP\GraphQL\Builder\Contracts\TypeProvider;
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 Nuwave\Lighthouse\Execution\Arguments\Argument;
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;

use function array_keys;
use function count;

abstract class PropertyDirective extends OperatorDirective {
public static function getName(): string {
return 'Property';
}

public function getFieldType(TypeProvider $provider, string $type): ?string {
return null;
}

public function isBuilderSupported(object $builder): bool {
return true;
}

public function call(Handler $handler, object $builder, Property $property, Argument $argument): 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(
array_keys($argument->value->arguments),
);
}

// Apply
return $handler->handle($builder, $property, $argument->value);
}
}
Loading

0 comments on commit 744b313

Please sign in to comment.