feat(graphql): sort pivot fields (#1003)

This commit is contained in:
Kyrch
2025-11-13 17:20:32 -03:00
committed by GitHub
parent f0437bf764
commit 0f290b1677
12 changed files with 161 additions and 176 deletions
@@ -11,7 +11,7 @@ use App\GraphQL\Support\ResolveInfo as CustomResolveInfo;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation as RelationLaravel;
use Illuminate\Database\Eloquent\Relations\Relation as EloquentRelation;
use Illuminate\Support\Arr;
trait ConstrainsEagerLoads
@@ -57,13 +57,13 @@ trait ConstrainsEagerLoads
$relationType = $relation->getBaseType();
$eagerLoadRelations[$path] = function (RelationLaravel $relationLaravel) use ($relationSelection, $relationArgs, $relationType): void {
if ($relationLaravel instanceof MorphTo) {
$this->processMorphToRelation($relationSelection, $relationType, $relationLaravel);
$eagerLoadRelations[$path] = function (EloquentRelation $eloquentRelation) use ($relationSelection, $relationArgs, $relationType, $relation): void {
if ($eloquentRelation instanceof MorphTo) {
$this->processMorphToRelation($relationSelection, $relationType, $eloquentRelation);
} elseif ($relationType instanceof BaseUnion) {
$this->processUnion($relationSelection, $relationLaravel, $relationType);
$this->processUnion($relationSelection, $eloquentRelation, $relationType);
} else {
$this->processGenericRelation($relationLaravel, $relationArgs, $relationSelection, $relationType);
$this->processGenericRelation($eloquentRelation, $relationArgs, $relationSelection, $relationType, $relation);
}
};
}
@@ -96,9 +96,9 @@ trait ConstrainsEagerLoads
/**
* Process a union relation by applying the eager loads for each type in the union.
*/
private function processUnion(array $selection, RelationLaravel $relation, BaseUnion $union): void
private function processUnion(array $selection, EloquentRelation $relation, BaseUnion $union): void
{
$query = $relation->getQuery();
$builder = $relation->getQuery();
$unions = Arr::get($selection, 'selectionSet.data.data.unions', []);
@@ -109,25 +109,25 @@ trait ConstrainsEagerLoads
foreach ($types as $type) {
$typeSelection = Arr::get($unions, "{$type->getName()}.selectionSet", []);
$this->processEagerLoadForType($query, $typeSelection, $type);
$this->processEagerLoadForType($builder, $typeSelection, $type);
}
}
/**
* Process a generic relation by applying filters, sorting and eager loads.
*/
private function processGenericRelation(RelationLaravel $relation, array $args, array $selection, BaseType $type): void
private function processGenericRelation(EloquentRelation $relation, array $args, array $selection, BaseType $type, Relation $graphqlRelation): void
{
$query = $relation->getQuery();
$builder = $relation->getQuery();
$this->filter($query, $args, $type);
$this->filter($builder, $args, $type);
$this->sort($query, $args, $type);
$this->sort($builder, $args, $type, $relation, $graphqlRelation);
$child = Arr::get($selection, 'selectionSet.data.data.selectionSet')
$fields = Arr::get($selection, 'selectionSet.data.data.selectionSet')
?? Arr::get($selection, 'selectionSet.nodes.nodes.selectionSet')
?? Arr::get($selection, 'selectionSet', []);
$this->processEagerLoadForType($query, $child, $type);
$this->processEagerLoadForType($builder, $fields, $type);
}
}
+9 -2
View File
@@ -9,12 +9,15 @@ use App\GraphQL\Schema\Enums\SortableColumns;
use App\GraphQL\Schema\Types\BaseType;
use App\GraphQL\Schema\Unions\BaseUnion;
use App\GraphQL\Support\Argument\SortArgument;
use App\GraphQL\Support\Relations\Relation as GraphQLRelation;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
trait SortsModels
{
public function sort(Builder $builder, array $args, BaseType|BaseUnion $type): Builder
public function sort(Builder $builder, array $args, BaseType|BaseUnion $type, ?Relation $relation = null, ?GraphQLRelation $graphqlRelation = null): Builder
{
$sorts = Arr::get($args, SortArgument::ARGUMENT);
@@ -22,7 +25,11 @@ trait SortsModels
return $builder;
}
$criterias = Arr::get(new SortableColumns($type)->getAttributes(), 'criterias');
$relation = $relation instanceof BelongsToMany
? $relation
: null;
$criterias = Arr::get(new SortableColumns($type, $graphqlRelation?->getPivotType(), $relation)->getAttributes(), 'criterias');
foreach ($sorts as $sort) {
/** @var SortCriteria $criteria */
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Criteria\Sort;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortDirection;
use App\GraphQL\Schema\Fields\Field;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Str;
class PivotSortCriteria extends SortCriteria
{
public function __construct(
protected Field&SortableField $field,
protected SortDirection $direction = SortDirection::ASC,
protected ?BelongsToMany $relation = null,
) {
parent::__construct($field, $direction);
}
/**
* Build the enum case for a direction.
* Template: PIVOT_{COLUMN}.
* Template: PIVOT_{COLUMN}_DESC.
*/
public function __toString(): string
{
return (string) match ($this->direction) {
SortDirection::ASC => Str::of($this->field->getName())->snake()->upper()->prepend('PIVOT_'),
SortDirection::DESC => Str::of($this->field->getName())->snake()->upper()->prepend('PIVOT_')->append('_DESC'),
};
}
/**
* Apply the ordering to the current Eloquent builder.
*/
public function sort(Builder $builder): Builder
{
return $builder->orderBy(
$this->relation?->qualifyPivotColumn($this->field->getColumn()),
$this->direction->value
);
}
}
+37 -5
View File
@@ -10,13 +10,16 @@ use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\AggregateSortCriteria;
use App\GraphQL\Criteria\Sort\CountSortCriteria;
use App\GraphQL\Criteria\Sort\FieldSortCriteria;
use App\GraphQL\Criteria\Sort\PivotSortCriteria;
use App\GraphQL\Criteria\Sort\RandomSortCriteria;
use App\GraphQL\Criteria\Sort\RelationSortCriteria;
use App\GraphQL\Criteria\Sort\SortCriteria;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Types\BaseType;
use App\GraphQL\Schema\Types\Pivot\PivotType;
use App\GraphQL\Support\Relations\BelongsToRelation;
use App\GraphQL\Support\Relations\Relation;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;
use Rebing\GraphQL\Support\EnumType;
@@ -27,15 +30,23 @@ class SortableColumns extends EnumType
{
final public const string SUFFIX = 'SortableColumns';
public function __construct(protected BaseType $type) {}
public function __construct(
protected BaseType $type,
protected ?PivotType $pivotType = null,
protected ?BelongsToMany $relation = null,
) {}
/**
* @return array<string, mixed>
*/
public function attributes(): array
{
$name = $this->pivotType instanceof PivotType
? $this->pivotType->getName()
: $this->type->getName();
return [
'name' => $this->type->getName().self::SUFFIX,
'name' => $name.self::SUFFIX,
'values' => $this->getCriterias()->mapWithKeys(fn (SortCriteria $sort): array => [$sort->__toString() => $sort->__toString()])->toArray(),
'criterias' => $this->getCriterias()->mapWithKeys(fn (SortCriteria $sort): array => [$sort->__toString() => $sort])->toArray(),
];
@@ -53,6 +64,7 @@ class SortableColumns extends EnumType
private function getCriterias(): Collection
{
return $this->getFieldSortCriterias()
->merge($this->getPivotSortCriterias())
->merge($this->getCountSortCriterias())
->merge($this->getAggregateSortCriterias())
->merge($this->getRelationSortCriterias())
@@ -63,7 +75,21 @@ class SortableColumns extends EnumType
{
return $this->getSortableFields()
->filter(fn (Field $field): bool => $field->sortType() === SortType::ROOT)
->map(fn (Field $field): array => [new FieldSortCriteria($field), new FieldSortCriteria($field, SortDirection::DESC)])
->map(fn (Field $field): array => [
new FieldSortCriteria($field),
new FieldSortCriteria($field, SortDirection::DESC),
])
->flatten();
}
private function getPivotSortCriterias(): Collection
{
return collect($this->pivotType?->fieldClasses() ?? [])
->filter(fn (Field $field): bool => $field instanceof SortableField)
->map(fn (Field $field): array => [
new PivotSortCriteria($field, SortDirection::ASC, $this->relation),
new PivotSortCriteria($field, SortDirection::DESC, $this->relation),
])
->flatten();
}
@@ -71,7 +97,10 @@ class SortableColumns extends EnumType
{
return $this->getSortableFields()
->filter(fn (Field $field): bool => $field->sortType() === SortType::COUNT_RELATION)
->map(fn (Field $field): array => [new CountSortCriteria($field), new CountSortCriteria($field, SortDirection::DESC)])
->map(fn (Field $field): array => [
new CountSortCriteria($field),
new CountSortCriteria($field, SortDirection::DESC),
])
->flatten();
}
@@ -79,7 +108,10 @@ class SortableColumns extends EnumType
{
return $this->getSortableFields()
->filter(fn (Field $field): bool => $field->sortType() === SortType::AGGREGATE)
->map(fn (Field $field): array => [new AggregateSortCriteria($field), new AggregateSortCriteria($field, SortDirection::DESC)])
->map(fn (Field $field): array => [
new AggregateSortCriteria($field),
new AggregateSortCriteria($field, SortDirection::DESC),
])
->flatten();
}
@@ -6,6 +6,7 @@ namespace App\GraphQL\Support\Argument;
use App\GraphQL\Schema\Enums\SortableColumns;
use App\GraphQL\Schema\Types\BaseType;
use App\GraphQL\Schema\Types\Pivot\PivotType;
use GraphQL\Type\Definition\Type;
use Illuminate\Support\Arr;
use Rebing\GraphQL\Support\Facades\GraphQL;
@@ -14,9 +15,11 @@ class SortArgument extends Argument
{
final public const string ARGUMENT = 'sort';
public function __construct(protected BaseType $type)
public function __construct(protected BaseType $type, ?PivotType $pivotType = null)
{
$sortableColumns = new SortableColumns($type);
$sortableColumns = new SortableColumns($type, $pivotType);
GraphQL::addType($sortableColumns);
$name = Arr::get($sortableColumns->getAttributes(), 'name');
@@ -8,6 +8,9 @@ use App\Enums\GraphQL\PaginationType;
use App\GraphQL\Schema\Types\ConnectionType;
use App\GraphQL\Schema\Types\EdgeType;
use App\GraphQL\Schema\Types\EloquentType;
use App\GraphQL\Schema\Types\Pivot\PivotType;
use App\GraphQL\Support\Argument\Argument;
use App\GraphQL\Support\Argument\SortArgument;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
@@ -20,7 +23,7 @@ class BelongsToManyRelation extends Relation
protected EloquentType $ownerType,
protected string $nodeType,
protected string $relationName,
protected ?string $pivotType = null,
protected PivotType|string|null $pivotType = null,
) {
$this->edgeType = new EdgeType($ownerType, $nodeType, $pivotType);
$this->connectionType = new ConnectionType($this->edgeType);
@@ -28,6 +31,28 @@ class BelongsToManyRelation extends Relation
GraphQL::addType($this->connectionType, $this->connectionType->getName());
parent::__construct(new $nodeType, $relationName);
$this->pivotType = class_exists($pivotType ?? '') ? new $pivotType : null;
}
/**
* Resolve the arguments of the sub-query.
*
* @return Argument[]
*/
protected function arguments(): array
{
$pivotType = $this->pivotType;
if ($pivotType instanceof PivotType) {
return [
...parent::arguments(),
new SortArgument($this->baseType, $pivotType),
];
}
return [];
}
/**
@@ -38,6 +63,14 @@ class BelongsToManyRelation extends Relation
return $this->edgeType;
}
/**
* Get the pivot type of the belongs to many relationship.
*/
public function getPivotType(): ?PivotType
{
return $this->pivotType;
}
public function type(): Type
{
return Type::nonNull(GraphQL::type($this->connectionType->getName()));
@@ -4,36 +4,4 @@ declare(strict_types=1);
namespace App\GraphQL\Support\Relations;
use App\Enums\GraphQL\PaginationType;
use GraphQL\Type\Definition\Type;
use Illuminate\Database\Eloquent\Model;
class HasOneRelation extends Relation
{
public function type(): Type
{
if (! $this->nullable) {
return Type::nonNull($this->type);
}
return $this->type;
}
/**
* The pagination type if applicable.
*/
public function paginationType(): PaginationType
{
return PaginationType::NONE;
}
/**
* Resolve the relation.
*
* @param array<string, mixed> $args
*/
public function resolve(Model $root, array $args): mixed
{
return $root->{$this->relationName};
}
}
class HasOneRelation extends BelongsToRelation {}
@@ -4,22 +4,4 @@ declare(strict_types=1);
namespace App\GraphQL\Support\Relations;
use App\Enums\GraphQL\PaginationType;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
class MorphManyRelation extends Relation
{
public function type(): Type
{
return Type::nonNull(Type::listOf(Type::nonNull(GraphQL::type($this->baseType->getName()))));
}
/**
* The pagination type if applicable.
*/
public function paginationType(): PaginationType
{
return PaginationType::SIMPLE;
}
}
class MorphManyRelation extends HasManyRelation {}
@@ -4,50 +4,4 @@ declare(strict_types=1);
namespace App\GraphQL\Support\Relations;
use App\Enums\GraphQL\PaginationType;
use App\GraphQL\Schema\Types\ConnectionType;
use App\GraphQL\Schema\Types\EdgeType;
use App\GraphQL\Schema\Types\EloquentType;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
class MorphToManyRelation extends Relation
{
protected EdgeType $edgeType;
protected ConnectionType $connectionType;
public function __construct(
protected EloquentType $ownerType,
protected string $nodeType,
protected string $relationName,
protected ?string $pivotType = null,
) {
$this->edgeType = new EdgeType($ownerType, $nodeType, $pivotType);
$this->connectionType = new ConnectionType($this->edgeType);
GraphQL::addType($this->connectionType, $this->connectionType->getName());
parent::__construct(new $nodeType, $relationName);
}
/**
* Get the edge type of the belongs to many relationship.
*/
public function getEdgeType(): EdgeType
{
return $this->edgeType;
}
public function type(): Type
{
return Type::nonNull(GraphQL::type($this->connectionType->getName()));
}
/**
* The pagination type if applicable.
*/
public function paginationType(): PaginationType
{
return PaginationType::CONNECTION;
}
}
class MorphToManyRelation extends BelongsToManyRelation {}
@@ -4,36 +4,4 @@ declare(strict_types=1);
namespace App\GraphQL\Support\Relations;
use App\Enums\GraphQL\PaginationType;
use GraphQL\Type\Definition\Type;
use Illuminate\Database\Eloquent\Model;
class MorphToRelation extends Relation
{
public function type(): Type
{
if (! $this->nullable) {
return Type::nonNull($this->type);
}
return $this->type;
}
/**
* The pagination type if applicable.
*/
public function paginationType(): PaginationType
{
return PaginationType::NONE;
}
/**
* Resolve the relation.
*
* @param array<string, mixed> $args
*/
public function resolve(Model $root, array $args): mixed
{
return $root->{$this->relationName};
}
}
class MorphToRelation extends BelongsToRelation {}
+9 -17
View File
@@ -9,6 +9,7 @@ use App\Concerns\GraphQL\ResolvesArguments;
use App\Enums\GraphQL\PaginationType;
use App\GraphQL\Schema\Fields\Base\DeletedAtField;
use App\GraphQL\Schema\Types\BaseType;
use App\GraphQL\Schema\Types\Pivot\PivotType;
use App\GraphQL\Schema\Unions\BaseUnion;
use App\GraphQL\Support\Argument\Argument;
use App\GraphQL\Support\Argument\FirstArgument;
@@ -106,28 +107,19 @@ abstract class Relation
return Arr::flatten($arguments);
}
/**
* @return array<string, array<string, mixed>>
*/
public function args(): array
{
return collect($this->arguments())
->mapWithKeys(fn (Argument $argument): array => [
$argument->name => [
'name' => $argument->name,
'type' => $argument->getType(),
...(is_null($argument->getDefaultValue()) ? [] : ['defaultValue' => $argument->getDefaultValue()]),
],
])
->toArray();
}
public function getBaseType(): BaseType|BaseUnion
{
return $this->baseType;
}
/**
* Get the pivot type if it exists.
*/
public function getPivotType(): ?PivotType
{
return null;
}
/**
* Resolve the relation.
*
+1 -2
View File
@@ -142,8 +142,7 @@ class Song extends BaseModel implements HasResources, SoftDeletable
*/
public function performances(): HasMany
{
return $this->hasMany(Performance::class, Performance::ATTRIBUTE_SONG)
->orderBy(Performance::ATTRIBUTE_RELEVANCE);
return $this->hasMany(Performance::class, Performance::ATTRIBUTE_SONG);
}
/**