diff --git a/app/Actions/GraphQL/IndexAction.php b/app/Actions/GraphQL/IndexAction.php index 8552a9a1d..460d0bd11 100644 --- a/app/Actions/GraphQL/IndexAction.php +++ b/app/Actions/GraphQL/IndexAction.php @@ -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, + ]; } } diff --git a/app/Actions/GraphQL/ShowAction.php b/app/Actions/GraphQL/ShowAction.php index 8b8b9ff17..04ed45d81 100644 --- a/app/Actions/GraphQL/ShowAction.php +++ b/app/Actions/GraphQL/ShowAction.php @@ -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); diff --git a/app/Concerns/Actions/GraphQL/AggregatesFields.php b/app/Concerns/Actions/GraphQL/AggregatesFields.php new file mode 100644 index 000000000..37a096bff --- /dev/null +++ b/app/Concerns/Actions/GraphQL/AggregatesFields.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php b/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php index e4ce8fcdf..f7cff4ce9 100644 --- a/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php +++ b/app/Concerns/Actions/GraphQL/ConstrainsEagerLoads.php @@ -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); diff --git a/app/Concerns/Actions/GraphQL/FieldSelection.php b/app/Concerns/Actions/GraphQL/FieldSelection.php new file mode 100644 index 000000000..25e81a83c --- /dev/null +++ b/app/Concerns/Actions/GraphQL/FieldSelection.php @@ -0,0 +1,20 @@ +getFieldSelectionWithAliases(100), "{$fieldName}.{$fieldName}.selectionSet") + ?? $resolveInfo->getFieldSelectionWithAliases(100); + } +} diff --git a/app/Contracts/GraphQL/Fields/SortableField.php b/app/Contracts/GraphQL/Fields/SortableField.php index e4375d267..5b13dc403 100644 --- a/app/Contracts/GraphQL/Fields/SortableField.php +++ b/app/Contracts/GraphQL/Fields/SortableField.php @@ -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; } diff --git a/app/Enums/GraphQL/Field/AggregateFunction.php b/app/Enums/GraphQL/Field/AggregateFunction.php new file mode 100644 index 000000000..e696fc08a --- /dev/null +++ b/app/Enums/GraphQL/Field/AggregateFunction.php @@ -0,0 +1,15 @@ +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 { diff --git a/app/GraphQL/Criteria/Sort/AggregateSortCriteria.php b/app/GraphQL/Criteria/Sort/AggregateSortCriteria.php deleted file mode 100644 index 0751e6cb5..000000000 --- a/app/GraphQL/Criteria/Sort/AggregateSortCriteria.php +++ /dev/null @@ -1,32 +0,0 @@ -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); - } -} diff --git a/app/GraphQL/Criteria/Sort/CountSortCriteria.php b/app/GraphQL/Criteria/Sort/CountSortCriteria.php deleted file mode 100644 index da7501f0f..000000000 --- a/app/GraphQL/Criteria/Sort/CountSortCriteria.php +++ /dev/null @@ -1,26 +0,0 @@ -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); - } -} diff --git a/app/GraphQL/Criteria/Sort/FieldSortCriteria.php b/app/GraphQL/Criteria/Sort/FieldSortCriteria.php index c243c8b9d..91138757e 100644 --- a/app/GraphQL/Criteria/Sort/FieldSortCriteria.php +++ b/app/GraphQL/Criteria/Sort/FieldSortCriteria.php @@ -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); } } diff --git a/app/GraphQL/Criteria/Sort/PivotSortCriteria.php b/app/GraphQL/Criteria/Sort/PivotSortCriteria.php index 9968ac3bc..b308b9405 100644 --- a/app/GraphQL/Criteria/Sort/PivotSortCriteria.php +++ b/app/GraphQL/Criteria/Sort/PivotSortCriteria.php @@ -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); } } diff --git a/app/GraphQL/Criteria/Sort/RelationSortCriteria.php b/app/GraphQL/Criteria/Sort/RelationSortCriteria.php index 556a1bfac..502e647fe 100644 --- a/app/GraphQL/Criteria/Sort/RelationSortCriteria.php +++ b/app/GraphQL/Criteria/Sort/RelationSortCriteria.php @@ -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); } } diff --git a/app/GraphQL/Criteria/Sort/Sort.php b/app/GraphQL/Criteria/Sort/Sort.php new file mode 100644 index 000000000..94c814258 --- /dev/null +++ b/app/GraphQL/Criteria/Sort/Sort.php @@ -0,0 +1,40 @@ +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; + } +} diff --git a/app/GraphQL/Criteria/Sort/SortCriteria.php b/app/GraphQL/Criteria/Sort/SortCriteria.php index cc2968a4c..6d4db59c4 100644 --- a/app/GraphQL/Criteria/Sort/SortCriteria.php +++ b/app/GraphQL/Criteria/Sort/SortCriteria.php @@ -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; diff --git a/app/GraphQL/Filter/Filter.php b/app/GraphQL/Filter/Filter.php index e99e581b5..5e6094f08 100644 --- a/app/GraphQL/Filter/Filter.php +++ b/app/GraphQL/Filter/Filter.php @@ -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[] */ diff --git a/app/GraphQL/Filter/RelationFilter.php b/app/GraphQL/Filter/RelationFilter.php deleted file mode 100644 index f4cd87e2b..000000000 --- a/app/GraphQL/Filter/RelationFilter.php +++ /dev/null @@ -1,38 +0,0 @@ - filter_var($filterValue, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) - ); - } - - /** - * Get the validation rules for the filter. - */ - protected function getRules(): array - { - return [ - 'required', - 'integer', - ]; - } -} diff --git a/app/GraphQL/Filter/TrashedFilter.php b/app/GraphQL/Filter/TrashedFilter.php index dac54386b..1e44ce2e6 100644 --- a/app/GraphQL/Filter/TrashedFilter.php +++ b/app/GraphQL/Filter/TrashedFilter.php @@ -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; diff --git a/app/GraphQL/Schema/Enums/SortableColumns.php b/app/GraphQL/Schema/Enums/SortableColumns.php index c06368621..d8455edcc 100644 --- a/app/GraphQL/Schema/Enums/SortableColumns.php +++ b/app/GraphQL/Schema/Enums/SortableColumns.php @@ -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) diff --git a/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailField.php b/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailField.php index a63248012..ab5756dd4 100644 --- a/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailField.php +++ b/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailField.php @@ -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; - } } diff --git a/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailVerifiedAtField.php b/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailVerifiedAtField.php index a9c77ea9c..02669edb5 100644 --- a/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailVerifiedAtField.php +++ b/app/GraphQL/Schema/Fields/Auth/User/Me/MeEmailVerifiedAtField.php @@ -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; - } } diff --git a/app/GraphQL/Schema/Fields/Auth/User/Me/MeNameField.php b/app/GraphQL/Schema/Fields/Auth/User/Me/MeNameField.php index 9fa342185..17691908f 100644 --- a/app/GraphQL/Schema/Fields/Auth/User/Me/MeNameField.php +++ b/app/GraphQL/Schema/Fields/Auth/User/Me/MeNameField.php @@ -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; - } } diff --git a/app/GraphQL/Schema/Fields/Auth/User/Me/MeTwoFactorConfirmedAtField.php b/app/GraphQL/Schema/Fields/Auth/User/Me/MeTwoFactorConfirmedAtField.php index dc4b2388d..84e6d0938 100644 --- a/app/GraphQL/Schema/Fields/Auth/User/Me/MeTwoFactorConfirmedAtField.php +++ b/app/GraphQL/Schema/Fields/Auth/User/Me/MeTwoFactorConfirmedAtField.php @@ -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; - } } diff --git a/app/GraphQL/Schema/Fields/Base/Aggregate/AggregateField.php b/app/GraphQL/Schema/Fields/Base/Aggregate/AggregateField.php new file mode 100644 index 000000000..e21120b07 --- /dev/null +++ b/app/GraphQL/Schema/Fields/Base/Aggregate/AggregateField.php @@ -0,0 +1,148 @@ +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; + } +} diff --git a/app/GraphQL/Schema/Fields/Base/Aggregate/CountAggregateField.php b/app/GraphQL/Schema/Fields/Base/Aggregate/CountAggregateField.php new file mode 100644 index 000000000..9b3c9b39b --- /dev/null +++ b/app/GraphQL/Schema/Fields/Base/Aggregate/CountAggregateField.php @@ -0,0 +1,44 @@ +getName(), $this->alias(), Clause::HAVING); + } + + public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed + { + return (int) parent::resolve(...func_get_args()); + } +} diff --git a/app/GraphQL/Schema/Fields/Base/Aggregate/CountField.php b/app/GraphQL/Schema/Fields/Base/Aggregate/CountField.php new file mode 100644 index 000000000..860337d17 --- /dev/null +++ b/app/GraphQL/Schema/Fields/Base/Aggregate/CountField.php @@ -0,0 +1,39 @@ +getName(), $this->alias(), Clause::HAVING); + } +} diff --git a/app/GraphQL/Schema/Fields/Base/Aggregate/ExistsField.php b/app/GraphQL/Schema/Fields/Base/Aggregate/ExistsField.php new file mode 100644 index 000000000..bb0901a4a --- /dev/null +++ b/app/GraphQL/Schema/Fields/Base/Aggregate/ExistsField.php @@ -0,0 +1,36 @@ +getName(), $this->alias(), Clause::HAVING); + } +} diff --git a/app/GraphQL/Schema/Fields/Base/LikesCountField.php b/app/GraphQL/Schema/Fields/Base/Aggregate/LikesCountField.php similarity index 88% rename from app/GraphQL/Schema/Fields/Base/LikesCountField.php rename to app/GraphQL/Schema/Fields/Base/Aggregate/LikesCountField.php index 85fd7a649..b5d496e6a 100644 --- a/app/GraphQL/Schema/Fields/Base/LikesCountField.php +++ b/app/GraphQL/Schema/Fields/Base/Aggregate/LikesCountField.php @@ -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; diff --git a/app/GraphQL/Schema/Fields/Base/CountAggregateField.php b/app/GraphQL/Schema/Fields/Base/CountAggregateField.php deleted file mode 100644 index 35ba09761..000000000 --- a/app/GraphQL/Schema/Fields/Base/CountAggregateField.php +++ /dev/null @@ -1,58 +0,0 @@ -aggregateRelation; - } - - /** - * Resolve the field. - * - * @param Model $root - */ - public function resolve($root, array $args, $context, ResolveInfo $resolveInfo): mixed - { - return (int) $root->{$this->aggregateRelation}?->value; - } -} diff --git a/app/GraphQL/Schema/Fields/Base/CountField.php b/app/GraphQL/Schema/Fields/Base/CountField.php deleted file mode 100644 index f9beb9a77..000000000 --- a/app/GraphQL/Schema/Fields/Base/CountField.php +++ /dev/null @@ -1,61 +0,0 @@ -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(); - } -} diff --git a/app/GraphQL/Schema/Fields/Base/ExistsField.php b/app/GraphQL/Schema/Fields/Base/ExistsField.php deleted file mode 100644 index 1460db97a..000000000 --- a/app/GraphQL/Schema/Fields/Base/ExistsField.php +++ /dev/null @@ -1,42 +0,0 @@ -{$this->relation}->isNotEmpty(); - } -} diff --git a/app/GraphQL/Schema/Fields/BooleanField.php b/app/GraphQL/Schema/Fields/BooleanField.php index c2bab5196..dd58685dc 100644 --- a/app/GraphQL/Schema/Fields/BooleanField.php +++ b/app/GraphQL/Schema/Fields/BooleanField.php @@ -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()); } } diff --git a/app/GraphQL/Schema/Fields/EnumField.php b/app/GraphQL/Schema/Fields/EnumField.php index 597daf17f..795e01252 100644 --- a/app/GraphQL/Schema/Fields/EnumField.php +++ b/app/GraphQL/Schema/Fields/EnumField.php @@ -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()); } } diff --git a/app/GraphQL/Schema/Fields/FloatField.php b/app/GraphQL/Schema/Fields/FloatField.php index ca0493a15..27acb5dfd 100644 --- a/app/GraphQL/Schema/Fields/FloatField.php +++ b/app/GraphQL/Schema/Fields/FloatField.php @@ -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()); } } diff --git a/app/GraphQL/Schema/Fields/IntField.php b/app/GraphQL/Schema/Fields/IntField.php index a55445aa7..945e819d2 100644 --- a/app/GraphQL/Schema/Fields/IntField.php +++ b/app/GraphQL/Schema/Fields/IntField.php @@ -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()); } } diff --git a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksCountField.php b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksCountField.php index 91899ca00..fcc806be9 100644 --- a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksCountField.php +++ b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksCountField.php @@ -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 diff --git a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksExistsField.php b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksExistsField.php index 831de2607..5b9e3352e 100644 --- a/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksExistsField.php +++ b/app/GraphQL/Schema/Fields/List/Playlist/PlaylistTracksExistsField.php @@ -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 diff --git a/app/GraphQL/Schema/Fields/StringField.php b/app/GraphQL/Schema/Fields/StringField.php index 65fae5c1e..c869c9ddb 100644 --- a/app/GraphQL/Schema/Fields/StringField.php +++ b/app/GraphQL/Schema/Fields/StringField.php @@ -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()); } } diff --git a/app/GraphQL/Schema/Fields/Wiki/Anime/Theme/Entry/AnimeThemeEntryTracksCountField.php b/app/GraphQL/Schema/Fields/Wiki/Anime/Theme/Entry/AnimeThemeEntryTracksCountField.php index 5463dd638..685eb844c 100644 --- a/app/GraphQL/Schema/Fields/Wiki/Anime/Theme/Entry/AnimeThemeEntryTracksCountField.php +++ b/app/GraphQL/Schema/Fields/Wiki/Anime/Theme/Entry/AnimeThemeEntryTracksCountField.php @@ -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 diff --git a/app/GraphQL/Schema/Fields/Wiki/Audio/AudioViewsCountField.php b/app/GraphQL/Schema/Fields/Wiki/Audio/AudioViewsCountField.php index c95d7dbac..dfed877e6 100644 --- a/app/GraphQL/Schema/Fields/Wiki/Audio/AudioViewsCountField.php +++ b/app/GraphQL/Schema/Fields/Wiki/Audio/AudioViewsCountField.php @@ -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 diff --git a/app/GraphQL/Schema/Fields/Wiki/Video/VideoViewsCountField.php b/app/GraphQL/Schema/Fields/Wiki/Video/VideoViewsCountField.php index be49bf862..c41d198c2 100644 --- a/app/GraphQL/Schema/Fields/Wiki/Video/VideoViewsCountField.php +++ b/app/GraphQL/Schema/Fields/Wiki/Video/VideoViewsCountField.php @@ -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 diff --git a/app/GraphQL/Schema/Inputs/WhereConditionsInput.php b/app/GraphQL/Schema/Inputs/WhereConditionsInput.php index 3580c1738..217056b5b 100644 --- a/app/GraphQL/Schema/Inputs/WhereConditionsInput.php +++ b/app/GraphQL/Schema/Inputs/WhereConditionsInput.php @@ -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; diff --git a/app/GraphQL/Schema/Types/List/PlaylistType.php b/app/GraphQL/Schema/Types/List/PlaylistType.php index 92f205aaa..c799953ff 100644 --- a/app/GraphQL/Schema/Types/List/PlaylistType.php +++ b/app/GraphQL/Schema/Types/List/PlaylistType.php @@ -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; diff --git a/app/GraphQL/Schema/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php b/app/GraphQL/Schema/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php index 57ebb3854..1a5e19276 100644 --- a/app/GraphQL/Schema/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php +++ b/app/GraphQL/Schema/Types/Wiki/Anime/Theme/AnimeThemeEntryType.php @@ -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; diff --git a/app/Http/Api/Field/Aggregate/AggregateField.php b/app/Http/Api/Field/Aggregate/AggregateField.php index 5abe2d53b..d3b590278 100644 --- a/app/Http/Api/Field/Aggregate/AggregateField.php +++ b/app/Http/Api/Field/Aggregate/AggregateField.php @@ -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 ) { diff --git a/app/Providers/GraphQLServiceProvider.php b/app/Providers/GraphQLServiceProvider.php index 9c37f6c12..c5948e5be 100644 --- a/app/Providers/GraphQLServiceProvider.php +++ b/app/Providers/GraphQLServiceProvider.php @@ -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;