feat(graphql): aggregates (#1061)

This commit is contained in:
Kyrch
2026-01-27 00:52:49 -03:00
committed by GitHub
parent 1f8b9daa1f
commit 01411c6f77
54 changed files with 495 additions and 408 deletions
+11 -9
View File
@@ -5,9 +5,9 @@ declare(strict_types=1);
namespace App\Actions\GraphQL;
use App\Concerns\Actions\GraphQL\ConstrainsEagerLoads;
use App\Concerns\Actions\GraphQL\FieldSelection;
use App\Concerns\Actions\GraphQL\PaginatesModels;
use App\Concerns\Actions\GraphQL\SortsModels;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Argument\SortArgument;
use App\GraphQL\Criteria\Sort\RelationSortCriteria;
use App\GraphQL\Criteria\Sort\SortCriteria;
@@ -26,11 +26,14 @@ use Illuminate\Support\Facades\Validator;
class IndexAction
{
use ConstrainsEagerLoads;
use FieldSelection;
use PaginatesModels;
use SortsModels;
public function index(Builder $builder, array $args, BaseType $type, ResolveInfo $resolveInfo): Paginator
{
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
$this->filter($builder, $args, $type);
$this->sort($builder, $args, $type);
@@ -46,6 +49,8 @@ class IndexAction
$searchBuilder = Search::search($builder->getModel(), $criteria)
->passToEloquentBuilder(function (Builder $builder) use ($args, $type, $resolveInfo): void {
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
$this->filter($builder, $args, $type);
$this->sort($builder, $args, $type);
@@ -81,22 +86,19 @@ class IndexAction
$column = $criterion->getField()->getColumn();
$direction = $criterion->getDirection();
$sortType = $criterion->getField()->sortType();
$isString = $criterion->getField() instanceof StringField;
if ($sortType === SortType::ROOT) {
$sortsRaw[$column] = [
'direction' => $direction,
'isString' => $isString,
];
}
if ($criterion instanceof RelationSortCriteria) {
$sortsRaw[$column] = [
'direction' => $direction->value,
'isString' => $isString,
'relation' => $criterion->relation,
];
} else {
$sortsRaw[$column] = [
'direction' => $direction,
'isString' => $isString,
];
}
}
+2
View File
@@ -18,6 +18,8 @@ class ShowAction
public function show(Builder $builder, array $args, BaseType $type, ResolveInfo $resolveInfo): Model
{
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
$this->filter($builder, $args, $type);
$this->constrainEagerLoads($builder, $resolveInfo, $type);
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Actions\GraphQL;
use App\GraphQL\Schema\Fields\Base\Aggregate\AggregateField;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Types\BaseType;
use App\GraphQL\Schema\Unions\BaseUnion;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
trait AggregatesFields
{
public function withAggregates(Builder $builder, array $args, array $selection, BaseType|BaseUnion $type): Builder
{
if ($type instanceof BaseUnion) {
return $builder;
}
collect($type->fieldClasses())
->filter(fn (Field $field): bool => $field instanceof AggregateField && $field->shouldAggregate($args, $selection, $type))
->each(fn (AggregateField $selectedAggregate): Builder => $selectedAggregate->with($builder));
return $builder;
}
public function loadAggregates(Model $model, array $args, array $selection, BaseType|BaseUnion $type): Model
{
if ($type instanceof BaseUnion) {
return $model;
}
collect($type->fieldClasses())
->filter(fn (Field $field): bool => $field instanceof AggregateField && $field->shouldAggregate($args, $selection, $type))
->each(fn (AggregateField $selectedAggregate): Model => $selectedAggregate->load($model));
return $model;
}
}
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Concerns\Actions\GraphQL;
use App\GraphQL\ResolveInfo as CustomResolveInfo;
use App\GraphQL\Schema\Relations\Relation;
use App\GraphQL\Schema\Types\BaseType;
use App\GraphQL\Schema\Types\EloquentType;
@@ -17,6 +16,8 @@ use Illuminate\Support\Arr;
trait ConstrainsEagerLoads
{
use AggregatesFields;
use FieldSelection;
use FiltersModels;
use SortsModels;
@@ -25,12 +26,7 @@ trait ConstrainsEagerLoads
*/
protected function constrainEagerLoads(Builder $query, ResolveInfo $resolveInfo, BaseType $type, string $fieldName = 'data'): void
{
$resolveInfo = new CustomResolveInfo($resolveInfo);
$selection = Arr::get($resolveInfo->getFieldSelectionWithAliases(100), "{$fieldName}.{$fieldName}.selectionSet")
?? $resolveInfo->getFieldSelectionWithAliases(100);
$this->processEagerLoadForType($query, $selection, $type);
$this->processEagerLoadForType($query, $this->getSelection($resolveInfo, $fieldName), $type);
}
/**
@@ -116,6 +112,8 @@ trait ConstrainsEagerLoads
$args = Arr::get($selection, 'args');
$this->withAggregates($builder, $args, $selection, $type);
$this->filter($builder, $args, $type);
$this->sort($builder, $args, $type, $relation, $graphqlRelation);
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Actions\GraphQL;
use App\GraphQL\ResolveInfo as GraphQLResolveInfo;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Support\Arr;
trait FieldSelection
{
protected function getSelection(ResolveInfo $resolveInfo, string $fieldName = 'data'): array
{
$resolveInfo = new GraphQLResolveInfo($resolveInfo);
return Arr::get($resolveInfo->getFieldSelectionWithAliases(100), "{$fieldName}.{$fieldName}.selectionSet")
?? $resolveInfo->getFieldSelectionWithAliases(100);
}
}
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Contracts\GraphQL\Fields;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\Sort;
interface SortableField
{
public function sortType(): SortType;
public function getSort(): Sort;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Enums\GraphQL\Field;
enum AggregateFunction: string
{
case AVG = 'avg';
case COUNT = 'count';
case EXISTS = 'exists';
case MAX = 'max';
case MIN = 'min';
case SUM = 'sum';
}
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Enums\GraphQL\Filter;
enum Clause
{
case WHERE;
case HAVING;
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enums\GraphQL;
namespace App\Enums\GraphQL\Filter;
use App\Concerns\Enums\CoercesInstances;
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enums\GraphQL;
namespace App\Enums\GraphQL\Filter;
use App\Concerns\Enums\CoercesInstances;
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enums\GraphQL;
namespace App\Enums\GraphQL\Filter;
use GraphQL\Type\Definition\Description;
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Enums\GraphQL;
enum QualifyColumn
{
case YES;
case NO;
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Enums\GraphQL;
namespace App\Enums\GraphQL\Sort;
enum SortDirection: string
{
-14
View File
@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Enums\GraphQL;
enum SortType: int
{
case NONE = 0;
case ROOT = 1;
case AGGREGATE = 2;
case RELATION = 3;
case COUNT_RELATION = 4;
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\GraphQL\Criteria\Filter;
use App\Enums\GraphQL\TrashedFilter;
use App\Enums\GraphQL\Filter\TrashedFilter;
use Illuminate\Database\Eloquent\Builder;
class TrashedFilterCriteria extends FilterCriteria
@@ -6,11 +6,11 @@ namespace App\GraphQL\Criteria\Filter;
use App\Concerns\Actions\GraphQL\FiltersModels;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Enums\GraphQL\ComparisonOperator;
use App\Enums\GraphQL\LogicalOperator;
use App\Enums\GraphQL\Filter\Clause;
use App\Enums\GraphQL\Filter\ComparisonOperator;
use App\Enums\GraphQL\Filter\LogicalOperator;
use App\GraphQL\Filter\Filter;
use App\GraphQL\Schema\Enums\FilterableColumns;
use App\GraphQL\Schema\Fields\Base\CountField;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Types\EloquentType;
use Illuminate\Database\Eloquent\Builder;
@@ -64,22 +64,20 @@ class WhereConditionsFilterCriteria extends FilterCriteria
if (filled($fieldName) && filled($value)) {
$field = $this->filterableFields->get($fieldName);
if ($field instanceof CountField) {
$builder->withCount(Str::replace('Count', '', $field->getName()));
$builder->having(
Str::snake($field->getColumn()),
ComparisonOperator::unstrictCoerce(Arr::get($where, 'operator'))->value,
Arr::first($field->getFilter()->getFilterValues(Arr::wrap($value))),
$logical->value
);
} else {
$builder->where(
match ($field->getFilter()->getClause()) {
Clause::WHERE => $builder->where(
$field->getColumn(),
ComparisonOperator::unstrictCoerce(Arr::get($where, 'operator'))->value,
Arr::first($field->getFilter()->getFilterValues(Arr::wrap($value))),
$logical->value
);
}
),
Clause::HAVING => $builder->having(
Str::snake($field->getColumn()),
ComparisonOperator::unstrictCoerce(Arr::get($where, 'operator'))->value,
Arr::first($field->getFilter()->getFilterValues(Arr::wrap($value))),
$logical->value
),
};
}
$builder->where(function (Builder $builder) use ($where): void {
@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Criteria\Sort;
use Exception;
use Illuminate\Database\Eloquent\Builder;
class AggregateSortCriteria extends SortCriteria
{
/**
* Apply the ordering to the current Eloquent builder.
*/
public function sort(Builder $builder): Builder
{
try {
/** @phpstan-ignore-next-line */
$relation = $this->field->{'relation'}();
} catch (Exception) {
throw new Exception("'relation' method is required for the aggregate sort type.");
}
$builder->withAggregate([
"$relation as {$relation}_value" => function ($query): void {
$query->orderBy('value', $this->direction->value);
},
], 'value');
return $builder->orderBy("{$relation}_value", $this->direction->value);
}
}
@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Criteria\Sort;
use Exception;
use Illuminate\Database\Eloquent\Builder;
class CountSortCriteria extends SortCriteria
{
/**
* Apply the ordering to the current Eloquent builder.
*/
public function sort(Builder $builder): Builder
{
try {
/** @phpstan-ignore-next-line */
$relation = $this->field->{'relation'}();
} catch (Exception) {
throw new Exception("'relation' method is required for the aggregate sort type");
}
return $builder->withCount($relation)->orderBy("{$relation}_count", $this->direction->value);
}
}
@@ -13,9 +13,12 @@ class FieldSortCriteria extends SortCriteria
*/
public function sort(Builder $builder): Builder
{
return $builder->orderBy(
$builder->qualifyColumn($this->field->getColumn()),
$this->direction->value
);
$sort = $this->field->getSort();
$column = $sort->shouldQualifyColumn()
? $builder->qualifyColumn($sort->getColumn())
: $sort->getColumn();
return $builder->orderBy($column, $this->direction->value);
}
}
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\GraphQL\Criteria\Sort;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortDirection;
use App\Enums\GraphQL\Sort\SortDirection;
use App\GraphQL\Schema\Fields\Field;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -44,9 +44,10 @@ class PivotSortCriteria extends SortCriteria
*/
public function sort(Builder $builder): Builder
{
return $builder->orderBy(
$this->relation?->qualifyPivotColumn($this->field->getColumn()),
$this->direction->value
);
$column = $this->field->getSort()->shouldQualifyColumn()
? $this->relation?->qualifyPivotColumn($this->field->getColumn())
: $this->field->getColumn();
return $builder->orderBy($column, $this->direction->value);
}
}
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\GraphQL\Criteria\Sort;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortDirection;
use App\Enums\GraphQL\Sort\SortDirection;
use App\GraphQL\Schema\Fields\Field;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
@@ -46,12 +46,6 @@ class RelationSortCriteria extends SortCriteria
{
$column = $this->field->getColumn();
$builder->withAggregate([
"{$this->relation} as {$this->relation}_$column" => function ($query) use ($column): void {
$query->orderBy($column, $this->direction->value);
},
], $column);
return $builder->orderBy("{$this->relation}_$column", $this->direction->value);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Criteria\Sort;
use App\Enums\GraphQL\QualifyColumn;
class Sort
{
public function __construct(
protected readonly string $key,
protected readonly ?string $column = null,
protected readonly QualifyColumn $qualifyColumn = QualifyColumn::YES
) {}
/**
* Get sort key value.
*/
public function getKey(): string
{
return $this->key;
}
/**
* Get sort column.
*/
public function getColumn(): string
{
return $this->column ?? $this->key;
}
/**
* Determine if the column should be qualified for the sort.
*/
public function shouldQualifyColumn(): bool
{
return $this->qualifyColumn === QualifyColumn::YES;
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\GraphQL\Criteria\Sort;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortDirection;
use App\Enums\GraphQL\Sort\SortDirection;
use App\GraphQL\Schema\Fields\Field;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
+7
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\GraphQL\Filter;
use App\Enums\GraphQL\Filter\Clause;
use App\Enums\Http\Api\Filter\ComparisonOperator;
use App\GraphQL\Argument\Argument;
use App\GraphQL\Argument\FilterArgument;
@@ -20,6 +21,7 @@ abstract class Filter
public function __construct(
protected readonly string $fieldName,
protected readonly ?string $column = null,
protected readonly Clause $clause = Clause::WHERE,
) {}
public function getFieldName(): string
@@ -32,6 +34,11 @@ abstract class Filter
return $this->column ?? $this->getFieldName();
}
public function getClause(): Clause
{
return $this->clause;
}
/**
* @return Argument[]
*/
-38
View File
@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Filter;
use GraphQL\Type\Definition\Type;
use Illuminate\Support\Arr;
class RelationFilter extends Filter
{
public function getBaseType(): Type
{
return Type::int();
}
/**
* Convert filter values if needed. By default, no conversion is needed.
*/
protected function convertFilterValues(array $filterValues): array
{
return Arr::map(
$filterValues,
fn (string $filterValue): ?int => filter_var($filterValue, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE)
);
}
/**
* Get the validation rules for the filter.
*/
protected function getRules(): array
{
return [
'required',
'integer',
];
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\GraphQL\Filter;
use App\Enums\GraphQL\TrashedFilter as TrashedFilterEnum;
use App\Enums\GraphQL\Filter\TrashedFilter as TrashedFilterEnum;
use App\GraphQL\Argument\Argument;
use Rebing\GraphQL\Support\Facades\GraphQL;
+1 -29
View File
@@ -5,10 +5,7 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Enums;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortDirection;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\AggregateSortCriteria;
use App\GraphQL\Criteria\Sort\CountSortCriteria;
use App\Enums\GraphQL\Sort\SortDirection;
use App\GraphQL\Criteria\Sort\FieldSortCriteria;
use App\GraphQL\Criteria\Sort\PivotSortCriteria;
use App\GraphQL\Criteria\Sort\RandomSortCriteria;
@@ -65,8 +62,6 @@ class SortableColumns extends EnumType
{
return $this->getFieldSortCriteria()
->merge($this->getPivotSortCriteria())
->merge($this->getCountSortCriteria())
->merge($this->getAggregateSortCriteria())
->merge($this->getRelationSortCriteria())
->push(new RandomSortCriteria());
}
@@ -74,7 +69,6 @@ class SortableColumns extends EnumType
private function getFieldSortCriteria(): Collection
{
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),
@@ -93,28 +87,6 @@ class SortableColumns extends EnumType
->flatten();
}
private function getCountSortCriteria(): Collection
{
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),
])
->flatten();
}
private function getAggregateSortCriteria(): Collection
{
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),
])
->flatten();
}
private function getRelationSortCriteria(): Collection
{
return $this->getRelations($this->type)
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Auth\User\Me;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Schema\Fields\StringField;
use App\Models\Auth\User;
@@ -19,9 +18,4 @@ class MeEmailField extends StringField
{
return 'The email of the user';
}
public function sortType(): SortType
{
return SortType::NONE;
}
}
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Auth\User\Me;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Schema\Fields\DateTimeTzField;
use App\Models\Auth\User;
@@ -19,9 +18,4 @@ class MeEmailVerifiedAtField extends DateTimeTzField
{
return 'The date the user verified their email';
}
public function sortType(): SortType
{
return SortType::NONE;
}
}
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Auth\User\Me;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Schema\Fields\StringField;
use App\Models\Auth\User;
@@ -19,9 +18,4 @@ class MeNameField extends StringField
{
return 'The username of authenticated user';
}
public function sortType(): SortType
{
return SortType::NONE;
}
}
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Auth\User\Me;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Schema\Fields\DateTimeTzField;
use App\Models\Auth\User;
@@ -19,9 +18,4 @@ class MeTwoFactorConfirmedAtField extends DateTimeTzField
{
return 'The date the user confirmed their two-factor authentication';
}
public function sortType(): SortType
{
return SortType::NONE;
}
}
@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base\Aggregate;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\Field\AggregateFunction;
use App\Enums\GraphQL\QualifyColumn;
use App\GraphQL\Criteria\Filter\FilterCriteria;
use App\GraphQL\Criteria\Filter\WhereConditionsFilterCriteria;
use App\GraphQL\Criteria\Sort\Sort;
use App\GraphQL\Criteria\Sort\SortCriteria;
use App\GraphQL\Schema\Enums\FilterableColumns;
use App\GraphQL\Schema\Enums\SortableColumns;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Types\BaseType;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
abstract class AggregateField extends Field implements FilterableField, SortableField
{
private Collection $filterableFields;
public function __construct(
public string $relation,
protected string $fieldName,
protected AggregateFunction $function,
protected string $aggregateColumn,
bool $nullable = true
) {
parent::__construct($this->alias(), $fieldName, $nullable);
}
public function getSort(): Sort
{
return new Sort($this->fieldName, $this->alias(), qualifyColumn: QualifyColumn::NO);
}
/**
* Eager load the aggregate value for the query builder.
*/
public function shouldAggregate(array $args, array $selection, BaseType $type): bool
{
// If the field is being requested.
if (Arr::has($selection, $this->getName())) {
return true;
}
// If the sorting is requesting an aggregate function.
/** @var SortCriteria[] $sortCriteria */
$sortCriteria = Arr::get(new SortableColumns($type)->getAttributes(), 'criteria');
$sorts = Arr::get($args, 'sort', []);
foreach ($sortCriteria as $sortCriterion) {
if (in_array($sortCriterion->__toString(), $sorts) && $sortCriterion->getField()->getName() === $this->getName()) {
return true;
}
}
// If the filtering is requesting an aggregate function.
$filterCriteria = FilterCriteria::parse($type, $args);
foreach ($filterCriteria as $filterCriterion) {
if ($filterCriterion instanceof WhereConditionsFilterCriteria) {
$this->filterableFields = new FilterableColumns($type)->getValues();
foreach ($filterCriterion->getFilterValues() as $filterValue) {
if ($this->recursiveWhere($filterValue)) {
return true;
}
}
}
}
return false;
}
/**
* Eager load the aggregate value for the query builder.
*/
public function with(Builder $builder): Builder
{
return $builder->withAggregate($this->relation, $this->aggregateColumn, $this->function->value);
}
/**
* Eager load the aggregate value for the query builder.
*/
public function load(Model $model): Model
{
return $model->loadAggregate($this->relation, $this->aggregateColumn, $this->function->value);
}
/**
* Resolve the field.
*
* @param Model $root
*/
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return $root->getAttribute($this->alias());
}
public function alias(): string
{
return Str::of($this->relation)
->snake()
->append('_')
->append($this->function->value)
->when($this->aggregateColumn !== '*', fn (Stringable $string) => $string->append('_')->append($this->aggregateColumn))
->__toString();
}
private function recursiveWhere(array $value): bool
{
$fieldName = Arr::get($value, 'field');
$fieldValue = Arr::get($value, 'value');
if ($fieldName && $fieldValue) {
$field = $this->filterableFields->get($fieldName);
if ($field instanceof Field && $field->getName() === $this->getName()) {
return true;
}
}
if ($and = Arr::get($value, 'AND')) {
foreach ($and as $andSolo) {
if ($this->recursiveWhere($andSolo)) {
return true;
}
}
}
if ($or = Arr::get($value, 'OR')) {
foreach ($or as $orSolo) {
if ($this->recursiveWhere($orSolo)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base\Aggregate;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\Field\AggregateFunction;
use App\Enums\GraphQL\Filter\Clause;
use App\GraphQL\Filter\IntFilter;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
class CountAggregateField extends AggregateField implements DisplayableField, SortableField
{
public function __construct(
public string $aggregateRelation,
string $name,
protected bool $nullable = false,
) {
parent::__construct($aggregateRelation, $name, AggregateFunction::SUM, 'value', $nullable);
}
public function baseType(): Type
{
return Type::int();
}
public function canBeDisplayed(): bool
{
return true;
}
public function getFilter(): IntFilter
{
return new IntFilter($this->getName(), $this->alias(), Clause::HAVING);
}
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return (int) parent::resolve(...func_get_args());
}
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base\Aggregate;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\Field\AggregateFunction;
use App\Enums\GraphQL\Filter\Clause;
use App\GraphQL\Filter\IntFilter;
use GraphQL\Type\Definition\Type;
class CountField extends AggregateField implements DisplayableField, FilterableField, SortableField
{
public function __construct(
protected string $aggregateRelation,
protected ?string $name = null,
protected bool $nullable = false,
) {
parent::__construct($aggregateRelation, $aggregateRelation.'Count', AggregateFunction::COUNT, '*', $nullable);
}
public function baseType(): Type
{
return Type::int();
}
public function canBeDisplayed(): bool
{
return true;
}
public function getFilter(): IntFilter
{
return new IntFilter($this->getName(), $this->alias(), Clause::HAVING);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base\Aggregate;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Enums\GraphQL\Field\AggregateFunction;
use App\Enums\GraphQL\Filter\Clause;
use App\GraphQL\Filter\BooleanFilter;
use GraphQL\Type\Definition\Type;
class ExistsField extends AggregateField implements DisplayableField
{
public function __construct(
protected string $aggregateRelation,
protected bool $nullable = false,
) {
parent::__construct($aggregateRelation, $aggregateRelation.'Exists', AggregateFunction::EXISTS, '*', $nullable);
}
public function baseType(): Type
{
return Type::boolean();
}
public function canBeDisplayed(): bool
{
return true;
}
public function getFilter(): BooleanFilter
{
return new BooleanFilter($this->getName(), $this->alias(), Clause::HAVING);
}
}
@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base;
namespace App\GraphQL\Schema\Fields\Base\Aggregate;
use App\Contracts\Models\HasAggregateLikes;
@@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Schema\Fields\Field;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Illuminate\Database\Eloquent\Model;
class CountAggregateField extends Field implements DisplayableField, SortableField
{
public function __construct(
public string $aggregateRelation,
protected string $column,
protected ?string $name = null,
protected bool $nullable = false,
) {
parent::__construct($column, $name, $nullable);
}
public function baseType(): Type
{
return Type::int();
}
public function canBeDisplayed(): bool
{
return true;
}
public function sortType(): SortType
{
return SortType::AGGREGATE;
}
/**
* The relation to sort the type.
*/
public function relation(): ?string
{
return $this->aggregateRelation;
}
/**
* Resolve the field.
*
* @param Model $root
*/
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return (int) $root->{$this->aggregateRelation}?->value;
}
}
@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Filter\RelationFilter;
use App\GraphQL\Schema\Fields\Field;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
class CountField extends Field implements DisplayableField, FilterableField, SortableField
{
public function __construct(
protected string $relation,
protected string $column,
protected ?string $name = null,
protected bool $nullable = false,
) {
parent::__construct($column, $name, $nullable);
}
public function baseType(): Type
{
return Type::int();
}
public function canBeDisplayed(): bool
{
return true;
}
public function sortType(): SortType
{
return SortType::COUNT_RELATION;
}
/**
* The relation to sort the type.
*/
public function relation(): ?string
{
return $this->relation;
}
public function getFilter(): RelationFilter
{
return new RelationFilter($this->getName(), $this->getColumn());
}
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return $root->hasAttribute($attribute = "{$this->relation}_count")
? (int) $root->getAttribute($attribute)
: $root->{$this->relation}->count();
}
}
@@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Base;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\GraphQL\Schema\Fields\Field;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Illuminate\Database\Eloquent\Model;
class ExistsField extends Field implements DisplayableField
{
public function __construct(
protected string $relation,
protected ?string $name = null,
protected bool $nullable = false,
) {
parent::__construct($relation.'Exists', $name, $nullable);
}
public function baseType(): Type
{
return Type::boolean();
}
public function canBeDisplayed(): bool
{
return true;
}
/**
* Resolve the field.
*
* @param Model $root
*/
public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed
{
return $root->{$this->relation}->isNotEmpty();
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ namespace App\GraphQL\Schema\Fields;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\Sort;
use App\GraphQL\Filter\BooleanFilter;
use GraphQL\Type\Definition\Type;
@@ -29,8 +29,8 @@ abstract class BooleanField extends Field implements DisplayableField, Filterabl
->useEq();
}
public function sortType(): SortType
public function getSort(): Sort
{
return SortType::ROOT;
return new Sort($this->getName(), $this->getColumn());
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ namespace App\GraphQL\Schema\Fields;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\Sort;
use App\GraphQL\Filter\EnumFilter;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
@@ -52,8 +52,8 @@ abstract class EnumField extends Field implements DisplayableField, FilterableFi
->useNotIn();
}
public function sortType(): SortType
public function getSort(): Sort
{
return SortType::ROOT;
return new Sort($this->getName(), $this->getColumn());
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ namespace App\GraphQL\Schema\Fields;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\Sort;
use App\GraphQL\Filter\FloatFilter;
use GraphQL\Type\Definition\Type;
@@ -33,8 +33,8 @@ abstract class FloatField extends Field implements DisplayableField, FilterableF
->useNotIn();
}
public function sortType(): SortType
public function getSort(): Sort
{
return SortType::ROOT;
return new Sort($this->getName(), $this->getColumn());
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ namespace App\GraphQL\Schema\Fields;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\Sort;
use App\GraphQL\Filter\IntFilter;
use GraphQL\Type\Definition\Type;
@@ -33,8 +33,8 @@ abstract class IntField extends Field implements DisplayableField, FilterableFie
->useNotIn();
}
public function sortType(): SortType
public function getSort(): Sort
{
return SortType::ROOT;
return new Sort($this->getName(), $this->getColumn());
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\List\Playlist;
use App\GraphQL\Schema\Fields\Base\CountField;
use App\GraphQL\Schema\Fields\Base\Aggregate\CountField;
use App\Models\List\Playlist;
class PlaylistTracksCountField extends CountField
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\List\Playlist;
use App\GraphQL\Schema\Fields\Base\ExistsField;
use App\GraphQL\Schema\Fields\Base\Aggregate\ExistsField;
use App\Models\List\Playlist;
class PlaylistTracksExistsField extends ExistsField
+3 -3
View File
@@ -7,7 +7,7 @@ namespace App\GraphQL\Schema\Fields;
use App\Contracts\GraphQL\Fields\DisplayableField;
use App\Contracts\GraphQL\Fields\FilterableField;
use App\Contracts\GraphQL\Fields\SortableField;
use App\Enums\GraphQL\SortType;
use App\GraphQL\Criteria\Sort\Sort;
use App\GraphQL\Filter\Filter;
use App\GraphQL\Filter\StringFilter;
use GraphQL\Type\Definition\Type;
@@ -31,8 +31,8 @@ abstract class StringField extends Field implements DisplayableField, Filterable
->useLike();
}
public function sortType(): SortType
public function getSort(): Sort
{
return SortType::ROOT;
return new Sort($this->getName(), $this->getColumn());
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Wiki\Anime\Theme\Entry;
use App\GraphQL\Schema\Fields\Base\CountField;
use App\GraphQL\Schema\Fields\Base\Aggregate\CountField;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
class AnimeThemeEntryTracksCountField extends CountField
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Wiki\Audio;
use App\Contracts\GraphQL\Fields\DeprecatedField;
use App\GraphQL\Schema\Fields\Base\CountAggregateField;
use App\GraphQL\Schema\Fields\Base\Aggregate\CountAggregateField;
use App\Models\Wiki\Audio;
class AudioViewsCountField extends CountAggregateField implements DeprecatedField
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Fields\Wiki\Video;
use App\Contracts\GraphQL\Fields\DeprecatedField;
use App\GraphQL\Schema\Fields\Base\CountAggregateField;
use App\GraphQL\Schema\Fields\Base\Aggregate\CountAggregateField;
use App\Models\Wiki\Video;
class VideoViewsCountField extends CountAggregateField implements DeprecatedField
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Inputs;
use App\Enums\GraphQL\ComparisonOperator;
use App\Enums\GraphQL\Filter\ComparisonOperator;
use App\GraphQL\Schema\Enums\FilterableColumns;
use App\GraphQL\Schema\Types\EloquentType;
use GraphQL\Type\Definition\Type;
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Types\List;
use App\GraphQL\Schema\Fields\Base\Aggregate\LikesCountField;
use App\GraphQL\Schema\Fields\Base\CreatedAtField;
use App\GraphQL\Schema\Fields\Base\LikesCountField;
use App\GraphQL\Schema\Fields\Base\UpdatedAtField;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Fields\List\Playlist\PlaylistDescriptionField;
@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace App\GraphQL\Schema\Types\Wiki\Anime\Theme;
use App\Contracts\GraphQL\Types\SubmitableType;
use App\GraphQL\Schema\Fields\Base\Aggregate\LikesCountField;
use App\GraphQL\Schema\Fields\Base\CreatedAtField;
use App\GraphQL\Schema\Fields\Base\DeletedAtField;
use App\GraphQL\Schema\Fields\Base\IdField;
use App\GraphQL\Schema\Fields\Base\LikesCountField;
use App\GraphQL\Schema\Fields\Base\UpdatedAtField;
use App\GraphQL\Schema\Fields\Field;
use App\GraphQL\Schema\Fields\Wiki\Anime\Theme\Entry\AnimeThemeEntryEpisodesField;
@@ -26,7 +26,7 @@ abstract class AggregateField extends Field implements FilterableField, Renderab
public function __construct(
Schema $schema,
protected readonly string $relation,
public readonly string $relation,
protected readonly AggregateFunction $function,
protected readonly string $aggregateColumn
) {
+3 -3
View File
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Providers;
use App\Enums\GraphQL\ComparisonOperator;
use App\Enums\GraphQL\SortDirection;
use App\Enums\GraphQL\TrashedFilter;
use App\Enums\GraphQL\Filter\ComparisonOperator;
use App\Enums\GraphQL\Filter\TrashedFilter;
use App\Enums\GraphQL\Sort\SortDirection;
use App\Enums\Models\List\ExternalEntryWatchStatus;
use App\Enums\Models\List\ExternalProfileSite;
use App\Enums\Models\List\ExternalProfileVisibility;