Skip to content

Commit

Permalink
feat(graphql)!: Type extensions improvements (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
LastDragon-ru authored Mar 1, 2024
2 parents 8fda774 + c136696 commit 1703d95
Show file tree
Hide file tree
Showing 32 changed files with 823 additions and 119 deletions.
24 changes: 21 additions & 3 deletions packages/graphql/UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases)

* [ ] `LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators::Condition` => `LastDragon_ru\LaraASP\GraphQL\SearchBy\Operators::Object`.

* [ ] `scalar SearchByCondition` => `scalar SearchByObject`.
* [ ] Scalars to add operators were renamed

* [ ] `SearchByCondition` => `SearchByOperatorsObject`

* [ ] `SearchByNull` => `SearchByOperatorsNull`

* [ ] `SearchByExtra` => `SearchByOperatorsExtra`

* [ ] `SearchByNumber` => `SearchByOperatorsNumber`

* [ ] `SearchByEnum` => `SearchByOperatorsEnum`

* [ ] Added the root type that will contain only extra operators and newly added `field` operator (always present and cannot be removed). The new query syntax is:

Expand Down Expand Up @@ -90,10 +100,12 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases)
2. Disable `LastDragon_ru\LaraASP\GraphQL\SearchBy\Definitions\SearchByOperatorFieldDirective` operator to avoid possible conflict with field names (via schema or config)

```graphql
scalar SearchByDisabled
scalar SearchByOperatorsDisabled
@searchByOperatorField
```

* [ ] If you define additional operators via `scalar SearchBy*` use `extend scalar SearchBy*` instead (or you will get `TypeDefinitionAlreadyDefined` error).

## `@sortBy`

* [ ] `enum SortByTypeFlag { yes }` => `enum SortByTypeFlag { Yes }`. 🤝
Expand Down Expand Up @@ -149,12 +161,18 @@ Please also see [changelog](https://github.com/LastDragon-ru/lara-asp/releases)
2. Disable `LastDragon_ru\LaraASP\GraphQL\SortBy\Definitions\SortByOperatorFieldDirective` operator to avoid possible conflict with field names (via schema or config)

```graphql
scalar SortByDisabled
scalar SortByOperatorsDisabled
@sortByOperatorField
```

* [ ] `@sortByOperatorRandom` cannot be added to `FIELD_DEFINITION` anymore.

* [ ] If you define addition operators via `scalar SortBy*` use `extend scalar SortBy*` instead (or you will get `TypeDefinitionAlreadyDefined` error).

* [ ] Scalars to add operators were renamed

* [ ] `SortByExtra` => `SortByOperatorsExtra`

## API

This section is actual only if you are extending the package. Please review and update (listed the most significant changes only):
Expand Down
23 changes: 15 additions & 8 deletions packages/graphql/docs/Directives/@searchBy.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ The `@searchByIgnored` can be used as Ignored marker.

```graphql
"""
Marks that field/definition should be excluded from search.
Marks that field/definition should be excluded.
"""
directive @searchByIgnored
on
Expand Down Expand Up @@ -208,24 +208,31 @@ By default, the package provide list of predefined operators for build-in GraphQ

The package also defines a few own types in addition to the standard GraphQL types:

* `SearchByObject` / [`Operators::Object`](../../src/SearchBy/Operators.php) - List of known operators for `Object`. If no other directive is found, the first supported operator from the list will be used.
* `SearchByNumber` / [`Operators::Number`](../../src/SearchBy/Operators.php) - Any operator for this type will be available for `Int` and `Float`.
* `SearchByNull` / [`Operators::Null`](../../src/SearchBy/Operators.php) - Additional operators available for nullable fields.
* `SearchByExtra` / [`Operators::Extra`](../../src/SearchBy/Operators.php) - List of additional extra operators for all types.
* `SearchByEnum` / [`Operators::Enum`](../../src/SearchBy/Operators.php) - Default operators for enums.
* `SearchByDisabled` / [`Operators::Disabled`](../../src/SearchBy/Operators.php) - Disabled operators.
* `SearchByOperatorsObject` / [`Operators::Object`](../../src/SearchBy/Operators.php) - List of known operators for `Object`. If no other directive is found, the first supported operator from the list will be used.
* `SearchByOperatorsNumber` / [`Operators::Number`](../../src/SearchBy/Operators.php) - Any operator for this type will be available for `Int` and `Float`.
* `SearchByOperatorsNull` / [`Operators::Null`](../../src/SearchBy/Operators.php) - Additional operators available for nullable fields.
* `SearchByOperatorsExtra` / [`Operators::Extra`](../../src/SearchBy/Operators.php) - List of additional extra operators for all types.
* `SearchByOperatorsEnum` / [`Operators::Enum`](../../src/SearchBy/Operators.php) - Default operators for enums.
* `SearchByOperatorsDisabled` / [`Operators::Disabled`](../../src/SearchBy/Operators.php) - Disabled operators.

### GraphQL
### GraphQL (recommended)

```graphql
extend scalar SearchByOperatorsEnum
@searchByExtendOperators # Re-use operators for `SearchByOperatorsEnum` from config
@searchByExtendOperators(type: "MyScalar") # Re-use operators from `MyScalar` from schema

scalar MyScalar
@scalar(class: "App\\GraphQL\\Scalars\\MyScalar")
@searchByExtendOperators # Re-use operators for `MyScalar` from config
@searchByExtendOperators(type: "MyScalar") # same
@searchByExtendOperators(type: "Int") # Re-use operators from `Int` from schema
@searchByOperatorEqual # Add package operator
@myOperator # Add custom operator
```

Keep in mind, when you define/extend the scalar/enum, it will override all existing operators, so if you just want to add new operators, the `@searchByExtendOperators` directive should be used.

[include:exec]: <../../../../dev/artisan dev:directive @searchByExtendOperators>
[//]: # (start: fb9508c1688c78899393b1119463a14ebcc2c0872316ca676b2945a296312230)
[//]: # (warning: Generated automatically. Do not edit.)
Expand Down
19 changes: 10 additions & 9 deletions packages/graphql/docs/Directives/@sortBy.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The `@sortByIgnored` can be used as Ignored marker.

```graphql
"""
Marks that field/definition should be excluded from sort.
Marks that field/definition should be excluded.
"""
directive @sortByIgnored
on
Expand All @@ -98,14 +98,22 @@ on

The package defines only one's own type. To extend/replace the list of its operators, you can use config and/or add directives to scalar/enum inside the schema. Directives is the recommended way and have priority over the config. Please see [`@searchBy`](@searchBy.md#type-operators) for examples.

* `SortByExtra` / [`Operators::Extra`](../../src/SortBy/Operators.php) - List of additional extra operators for all types. The list is empty by default.
* `SortByOperatorsExtra` / [`Operators::Extra`](../../src/SortBy/Operators.php) - List of additional extra operators for all types. The list is empty by default.
* `SortByOperatorsDisabled` / [`Operators::Disabled`](../../src/SortBy/Operators.php) - Disabled operators.

## Eloquent/Database

### Order by random

It is also possible to sort records in random order, but it is not enabled by default. To enable it you just need to add [`Random`](../../src/SortBy/Operators/Extra/Random.php)/`@sortByOperatorRandom` operator/directive to `Extra` type:

```graphql
extend scalar SortByOperatorsExtra
@sortByOperatorRandom
```

or via config

```php
<?php declare(strict_types = 1);

Expand Down Expand Up @@ -137,13 +145,6 @@ $settings = [
return $settings;
```

or

```graphql
scalar SortByExtra
@sortByOperatorRandom
```

And after this, you can 🎉

```graphql
Expand Down
44 changes: 44 additions & 0 deletions packages/graphql/src/Builder/Directives/IgnoredDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Directives;

use GraphQL\Language\DirectiveLocation;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Override;

use function array_unique;
use function implode;

abstract class IgnoredDirective extends BaseDirective {
public function __construct() {
// empty
}

#[Override]
public static function definition(): string {
$name = DirectiveLocator::directiveName(static::class);
$locations = implode(' | ', array_unique(static::getDirectiveLocations()));

return <<<GRAPHQL
"""
Marks that field/definition should be excluded.
"""
directive @{$name} on {$locations}
GRAPHQL;
}

/**
* @return non-empty-list<string>
*/
protected static function getDirectiveLocations(): array {
return [
DirectiveLocation::FIELD_DEFINITION,
DirectiveLocation::INPUT_FIELD_DEFINITION,
DirectiveLocation::OBJECT,
DirectiveLocation::INPUT_OBJECT,
DirectiveLocation::ENUM,
DirectiveLocation::SCALAR,
];
}
}
179 changes: 179 additions & 0 deletions packages/graphql/src/Builder/Directives/SchemaDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\GraphQL\Builder\Directives;

use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeExtensionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeExtensionNode;
use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Language\Parser;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\AST;
use Illuminate\Support\Str;
use LastDragon_ru\LaraASP\Core\Utils\Cast;
use LastDragon_ru\LaraASP\GraphQL\Builder\Exceptions\TypeDefinitionIsNotScalarExtension;
use LastDragon_ru\LaraASP\GraphQL\Builder\Scalars\Internal;
use LastDragon_ru\LaraASP\GraphQL\Builder\Traits\WithManipulator;
use Nuwave\Lighthouse\Events\BuildSchemaString;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
use Override;

use function count;
use function implode;
use function in_array;
use function str_starts_with;

/**
* Modifies the Schema for Directive.
*
* We are using special scalars to add operators. The directive provides a way
* to add and extend them. Extending is required because Lighthouse doesn't
* support adding directives from extensions nodes yet.
*
* @see https://github.com/nuwave/lighthouse/issues/2509
* @see https://github.com/nuwave/lighthouse/pull/2512
*
* @internal
*/
abstract class SchemaDirective extends BaseDirective implements TypeManipulator {
use WithManipulator;

#[Override]
public static function definition(): string {
$name = DirectiveLocator::directiveName(static::class);
$locations = implode(' | ', [DirectiveLocation::SCALAR]);

return <<<GRAPHQL
"""
Extends schema for Directive.
"""
directive @{$name} on {$locations}
GRAPHQL;
}

public function __invoke(BuildSchemaString $event): string {
$name = DirectiveLocator::directiveName(static::class);
$scalar = $this->getScalarDefinition(Str::studly($name));
$directive = "@{$name}";

return "{$scalar} {$directive}";
}

#[Override]
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void {
// Apply `extend scalar`.
$manipulator = $this->getAstManipulator($documentAST);

foreach ($documentAST->typeExtensions as $type => $extensions) {
// Supported?
// (no way to extend standard types, we are trying to use alias instead)
$targetType = ($manipulator->isStandard($type) ? $this->getScalar() : '').$type;

if (!$this->isScalar($targetType)) {
continue;
}

// Extend
$targetNode = $manipulator->addTypeDefinition($this->getScalarDefinitionNode($targetType));

foreach ($extensions as $key => $extension) {
// Valid?
if (!($extension instanceof ScalarTypeExtensionNode)) {
throw new TypeDefinitionIsNotScalarExtension($targetType, $this->getExtensionNodeName($extension));
}

// Directives
if ($targetType === $type) {
$targetNode->directives = $targetNode->directives->merge($extension->directives);
} else {
// Only known directives will be copied for alias
foreach ($extension->directives as $index => $directive) {
if ($this->isDirective($directive->name->value)) {
$targetNode->directives[] = $directive;

unset($extension->directives[$index]);
}
}

$extension->directives->reindex();
}

// Remove to avoid conflicts with future Lighthouse version
unset($documentAST->typeExtensions[$type][$key]);

if (count($documentAST->typeExtensions[$type]) === 0) {
unset($documentAST->typeExtensions[$type]);
}
}
}

// Remove self
$manipulator->removeTypeDefinition($typeDefinition->getName()->value);
}

protected function isDirective(string $name): bool {
return str_starts_with($name, $this->getDirective());
}

abstract protected function getDirective(): string;

abstract protected function getScalar(): string;

/**
* @return array<array-key, string>
*/
abstract protected function getScalars(): array;

protected function isScalar(string $name): bool {
return $name !== $this->getScalar()
&& str_starts_with($name, $this->getScalar())
&& in_array($name, $this->getScalars(), true);
}

protected function getScalarDefinition(string $name): string {
$class = Internal::class;
$value = Cast::to(Node::class, AST::astFromValue($class, Type::string()));
$value = Printer::doPrint($value);
$scalar = <<<GRAPHQL
"""
The scalar is used to add builder operators for `@{$this->getDirective()}` directive.
"""
scalar {$name}
@scalar(
class: {$value}
)
GRAPHQL;

return $scalar;
}

protected function getScalarDefinitionNode(string $name): ScalarTypeDefinitionNode {
return Parser::scalarTypeDefinition($this->getScalarDefinition($name));
}

private function getExtensionNodeName(TypeExtensionNode $node): string {
$name = $node->getName()->value;
$type = match (true) {
$node instanceof EnumTypeExtensionNode => 'enum',
$node instanceof InputObjectTypeExtensionNode => 'input',
$node instanceof InterfaceTypeExtensionNode => 'interface',
$node instanceof ObjectTypeExtensionNode => 'object',
$node instanceof ScalarTypeExtensionNode => 'scalar',
$node instanceof UnionTypeExtensionNode => 'union',
default => 'unknown',
};

return "extend {$type} {$name}";
}
}
Loading

0 comments on commit 1703d95

Please sign in to comment.