diff --git a/CHANGELOG.md b/CHANGELOG.md index cd34f65..7b7bf04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `ag-grid-laravel` will be documented in this file. +## Unreleased + +### Improved + +- Added support for nested relations in filters + ## 0.2.0 (2023-08-24) - Rename `$params` to `$filters` in `AgGridCustomFilterable` diff --git a/README.md b/README.md index 1d411c9..53fddc9 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ function onFilterChanged(event: FilterChangedEvent) { - Only works with PostgreSQL as a storage backend due to some special SQL operators being used in set and json queries. - Does not support multiple conditions per filter (AND, OR) - Does not support server-side grouping for AG Grid's pivot mode -- Filtering for values in relations is only supported one level deep. E.g you can filter for `relation.value` but not `relation.otherRelation.value` +- ~~Filtering for values in relations is only supported one level deep. E.g you can filter for `relation.value` but not `relation.otherRelation.value`~~ ## TODOs diff --git a/src/AgGridExport.php b/src/AgGridExport.php index f364c0d..1842491 100644 --- a/src/AgGridExport.php +++ b/src/AgGridExport.php @@ -15,7 +15,7 @@ use Maatwebsite\Excel\Concerns\WithMapping; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -class AgGridExport implements FromQuery, WithHeadings, WithColumnFormatting, ShouldAutoSize, WithMapping +class AgGridExport implements FromQuery, ShouldAutoSize, WithColumnFormatting, WithHeadings, WithMapping { /** * @var Collection diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index 4bdeb07..bd6c342 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -9,7 +9,12 @@ use Clickbar\AgGrid\Enums\AgGridNumberFilterType; use Clickbar\AgGrid\Enums\AgGridRowModel; use Clickbar\AgGrid\Enums\AgGridTextFilterType; +use Clickbar\AgGrid\Exceptions\InvalidSetValueOperation; +use Clickbar\AgGrid\Exceptions\UnauthorizedSetFilterColumn; use Clickbar\AgGrid\Requests\AgGridGetRowsRequest; +use Clickbar\AgGrid\Requests\AgGridSetValuesRequest; +use Clickbar\AgGrid\Support\ColumnMetadata; +use Clickbar\AgGrid\Support\RowGroupMetadata; use Illuminate\Contracts\Support\Responsable; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; @@ -17,7 +22,9 @@ use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceCollection; -use Illuminate\Support\Str; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Traits\ForwardsCalls; use Maatwebsite\Excel\Facades\Excel; @@ -35,6 +42,8 @@ class AgGridQueryBuilder implements Responsable /** @var class-string | null */ protected ?string $resourceClass = null; + protected RowGroupMetadata $rowGroupMetadata; + /** * @param EloquentBuilder|Relation|Model|class-string $subject */ @@ -52,10 +61,13 @@ public function __construct(array $params, EloquentBuilder|Relation|Model|string $model->applyAgGridCustomFilters($this->subject, $this->params['customFilters'] ?? []); } + $this->rowGroupMetadata = RowGroupMetadata::fromParams($params); + $this->addFiltersToQuery(); $this->addToggledFilterToQuery(); $this->addSortsToQuery(); $this->addLimitAndOffsetToQuery(); + $this->addRowGrouping(); } /** @@ -68,6 +80,16 @@ public static function forRequest(AgGridGetRowsRequest $request, EloquentBuilder return new AgGridQueryBuilder($request->validated(), $subject); } + /** + * Returns a new AgGridQueryBuilder for an AgGridGetRowsRequest. + * + * @param EloquentBuilder|Relation|Model|class-string $subject + */ + public static function forSetValuesRequest(AgGridSetValuesRequest $request, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder + { + return new AgGridQueryBuilder($request->validated(), $subject); + } + /** * Returns a new AgGridQueryBuilder for a selection. * @@ -102,6 +124,40 @@ public function resource(string $resourceClass): self return $this; } + public function toSetValues(array $allowedColumns = []): Collection + { + $column = Arr::get($this->params, 'column'); + if (empty($column)) { + throw InvalidSetValueOperation::make(); + } + + if (collect($allowedColumns)->first() !== '*' && ! in_array($column, $allowedColumns)) { + throw UnauthorizedSetFilterColumn::make($column); + } + + $columnMetadata = ColumnMetadata::fromString($this->subject, $column); + + if ($columnMetadata->hasRelations()) { + + $dottedRelation = $columnMetadata->getDottedRelation(); + + return $this->subject->with($dottedRelation) + ->get() + ->map(fn (Model $model) => Arr::get($this->traverse($model, $dottedRelation)->toArray(), $columnMetadata->getColumn())) + ->unique() + ->sort() + ->values(); + } + + $column = $columnMetadata->isJsonColumn() ? $columnMetadata->getColumnAsJsonPath() : $columnMetadata->getColumn(); + + return $this->subject + ->select($column) + ->distinct() + ->orderBy($column) + ->pluck($column); + } + public function __call($name, $arguments) { $result = $this->forwardCallTo($this->subject, $name, $arguments); @@ -141,7 +197,15 @@ public function toResponse($request): mixed $query->limit = $query->offset = $query->orders = null; $query->cleanBindings(['order']); }); - $total = $clone->count(); + + if ($this->isGrouped()) { + // TODO: Check for better way + $total = DB::query() + ->fromSub($clone, 'rows') + ->count(); + } else { + $total = $clone->count(); + } $data = $this->get(); @@ -191,6 +255,11 @@ protected function addServerSideToggledFilterToQuery(): void } } + protected function addRowGrouping(): void + { + $this->rowGroupMetadata->appendQueryBuilderMethods($this->subject); + } + protected function addFiltersToQuery(): void { if (! isset($this->params['filterModel'])) { @@ -199,16 +268,22 @@ protected function addFiltersToQuery(): void $filters = collect($this->params['filterModel']); + // Check if we are in set values mode and exclude the filter for the given set value column + $column = Arr::get($this->params, 'column'); + if ($column) { + $filters = $filters->filter(fn ($value, $key) => $key !== $column); + } + foreach ($filters as $column => $filter) { - [$relation, $column] = $this->getRelation($column); + $columnInformation = ColumnMetadata::fromString($this->subject, $column); - if ($relation !== null) { - $this->subject->whereHas($relation, function (EloquentBuilder $builder) use ($column, $filter) { - $this->addFilterToQuery($builder, $column, $filter); + if ($columnInformation->hasRelations()) { + $this->subject->whereHas($columnInformation->getDottedRelation(), function (EloquentBuilder $builder) use ($columnInformation, $filter) { + $this->addFilterToQuery($builder, $columnInformation, $filter); }); } else { - $this->addFilterToQuery($this->subject, $column, $filter); + $this->addFilterToQuery($this->subject, $columnInformation, $filter); } } } @@ -227,11 +302,23 @@ protected function addSortsToQuery(): void } foreach ($sorts as $sort) { - $this->subject->orderBy($this->toJsonPath($sort['colId']), $sort['sort']); + + // Check if the sort field is included in the current grouping + if ($this->rowGroupMetadata->isColumnAvailable($sort['colId'])) { + $this->subject->orderBy($this->toJsonPath($sort['colId']), $sort['sort']); + } + } + + if ($this->rowGroupMetadata->isGrouped()) { + // TODO: Check for current grouping level + + // TODO: Add more context for better accessibility of groups + $this->subject->orderBy($this->rowGroupMetadata->getCurrentRowGroupCol()['field']); + } else { + // we need an additional sort condition so that the order is stable in all cases + $this->subject->orderBy($this->subject->getModel()->getKeyName()); } - // we need an additional sort condition so that the order is stable in all cases - $this->subject->orderBy($this->subject->getModel()->getKeyName()); } protected function addLimitAndOffsetToQuery(): void @@ -246,21 +333,21 @@ protected function addLimitAndOffsetToQuery(): void $this->subject->offset($startRow)->limit($endRow - $startRow); } - protected function addFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { $filterType = AgGridFilterType::from($filter['filterType']); match ($filterType) { - AgGridFilterType::Set => $this->addSetFilterToQuery($subject, $column, $filter), - AgGridFilterType::Text => $this->addTextFilterToQuery($subject, $column, $filter), - AgGridFilterType::Number => $this->addNumberFilterToQuery($subject, $column, $filter), - AgGridFilterType::Date => $this->addDateFilterToQuery($subject, $column, $filter), + AgGridFilterType::Set => $this->addSetFilterToQuery($subject, $columnInformation, $filter), + AgGridFilterType::Text => $this->addTextFilterToQuery($subject, $columnInformation, $filter), + AgGridFilterType::Number => $this->addNumberFilterToQuery($subject, $columnInformation, $filter), + AgGridFilterType::Date => $this->addDateFilterToQuery($subject, $columnInformation, $filter), }; } - protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $isJsonColumn = $this->isJsonColumn($column); - $column = $this->toJsonPath($column); + $isJsonColumn = $columnInformation->isJsonColumn(); + $column = $columnInformation->getColumnAsJsonPath(); $values = $filter['values']; $all = $filter['all'] ?? false; $filteredValues = array_filter($values, fn ($value) => $value !== null); @@ -284,9 +371,9 @@ protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, string }); } - protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $column = $this->toJsonPath($column); + $column = $columnInformation->getColumnAsJsonPath(); $value = $filter['filter'] ?? null; $type = AgGridTextFilterType::from($filter['type']); @@ -302,9 +389,9 @@ protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, strin }; } - protected function addNumberFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addNumberFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $column = $this->toJsonPath($column); + $column = $columnInformation->getColumnAsJsonPath(); $value = $filter['filter']; $type = AgGridNumberFilterType::from($filter['type']); @@ -321,9 +408,9 @@ protected function addNumberFilterToQuery(EloquentBuilder|Relation $subject, str }; } - protected function addDateFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addDateFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $column = $this->toJsonPath($column); + $column = $columnInformation->getColumnAsJsonPath(); $dateFrom = isset($filter['dateFrom']) ? new \DateTime($filter['dateFrom']) : null; $dateTo = isset($filter['dateTo']) ? new \DateTime($filter['dateTo']) : null; @@ -338,37 +425,50 @@ protected function addDateFilterToQuery(EloquentBuilder|Relation $subject, strin }; } - protected function getRelation(string $column): array + protected function toJsonPath(string $key): string { - $pos = strpos($column, '.'); - if ($pos === false) { - return [null, $column]; + return str_replace('.', '->', $key); + } + + protected function traverse($model, $key, $default = null): Model + { + if (is_array($model)) { + return Arr::get($model, $key, $default); } - // guess the name of the relation - $relationName = Str::camel(substr($column, 0, $pos)); - if ($this->subject->getModel()->isRelation($relationName)) { - return [$relationName, substr($column, $pos + 1)]; + + if (is_null($key)) { + return $model; } - return [null, $column]; - } + if (isset($model[$key])) { + return $model[$key]; + } - protected function isJsonColumn(string $column): bool - { - return str_contains($column, '.') || $this->subject->getModel()->hasCast($column, [ - 'array', - 'json', - 'object', - 'collection', - 'encrypted:array', - 'encrypted:collection', - 'encrypted:json', - 'encrypted:object', - ]); + foreach (explode('.', $key) as $segment) { + try { + $model = $model->$segment; + } catch (\Exception $e) { // @phpstan-ignore-line + return value($default); + } + } + + return $model; } - protected function toJsonPath(string $key): string + protected function isGrouped(): bool { - return str_replace('.', '->', $key); + if (! isset($this->params['rowGroupCols']) || empty($this->params['rowGroupCols'])) { + return false; + } + + // --> rowGroupCols available and not empty + + if (! isset($this->params['groupKeys']) || empty($this->params['groupKeys'])) { + return true; + } + + // --> groupKeys available and not empty + + return count($this->params['rowGroupCols']) !== count($this->params['groupKeys']); } } diff --git a/src/Exceptions/InvalidSetValueOperation.php b/src/Exceptions/InvalidSetValueOperation.php new file mode 100644 index 0000000..b30f02b --- /dev/null +++ b/src/Exceptions/InvalidSetValueOperation.php @@ -0,0 +1,13 @@ + ['sometimes', 'boolean'], 'toggledNodes' => ['sometimes', 'array'], 'customFilters' => ['sometimes', 'array'], + // Row Grouping + 'rowGroupCols' => ['sometimes', 'array'], + 'groupKeys' => ['sometimes', 'array'], ]; } } diff --git a/src/Requests/AgGridSetValuesRequest.php b/src/Requests/AgGridSetValuesRequest.php new file mode 100644 index 0000000..81a87d0 --- /dev/null +++ b/src/Requests/AgGridSetValuesRequest.php @@ -0,0 +1,16 @@ + ['required', 'string'], + 'filterModel' => ['sometimes', 'array'], + ]; + } +} diff --git a/src/Support/ColumnMetadata.php b/src/Support/ColumnMetadata.php new file mode 100644 index 0000000..11b95f4 --- /dev/null +++ b/src/Support/ColumnMetadata.php @@ -0,0 +1,110 @@ +explode('.'); + + if ($parts->count() === 1) { + // --> No nested information + return new self($subject->getModel(), [], $column); + } + + $relations = []; + $model = $subject->getModel(); + + foreach ($parts as $index => $part) { + + $relationName = Str::camel($part); + $modelRelations = self::getRelations($model::class); + + if ($modelRelations->contains($relationName)) { + $relation = $model->$relationName(); + $model = $relation->getModel(); + + $relations[] = new RelationMetadata($relationName, $model); + } else { + // --> End of relation (further dots must be json nesting) + $remaining = $parts->skip($index + 1)->implode('.'); + if (! empty($remaining)) { + $remaining = '.'.$remaining; + } + $column = $part.$remaining; + break; + } + } + + return new self($subject->getModel(), $relations, $column); + + } + + protected static function getRelations(string $modelClass): Collection + { + return collect((new ReflectionClass($modelClass))->getMethods(ReflectionMethod::IS_PUBLIC)) + ->filter(function (ReflectionMethod $reflectionMethod) { + $returnType = (string) $reflectionMethod->getReturnType(); + + return $returnType != null && is_subclass_of($returnType, Relation::class); + }) + ->map(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->getName()); + } + + public function hasRelations(): bool + { + return ! empty($this->relations); + } + + public function getDottedRelation(): string + { + return collect($this->relations) + ->implode('name', '.'); + } + + public function getColumn(): string + { + return $this->column; + } + + public function isJsonColumn(): bool + { + $model = collect($this->relations)->last()?->model ?? $this->baseModel; + $colum = Str::before($this->column, '.'); + + return str_contains($this->column, '.') || $model->hasCast($colum, [ + 'array', + 'json', + 'object', + 'collection', + 'encrypted:array', + 'encrypted:collection', + 'encrypted:json', + 'encrypted:object', + ]); + + } + + public function getColumnAsJsonPath(): string + { + return str_replace('.', '->', $this->column); + } +} diff --git a/src/Support/RelationMetadata.php b/src/Support/RelationMetadata.php new file mode 100644 index 0000000..2ad4023 --- /dev/null +++ b/src/Support/RelationMetadata.php @@ -0,0 +1,14 @@ +groupKeys ?? []) - 1; + for ($index = 0; $index <= $lastEquippedRowGroupColIndex; $index++) { + $builder->where($this->rowGroupCols[$index]['field'], $this->groupKeys[$index]); + } + + // Add the group by column + $currentRowGroupCol = $this->getCurrentRowGroupCol(); + if ($currentRowGroupCol) { + $builder->cleanBindings(['select']); + $builder->select($currentRowGroupCol['field']); + $builder->groupBy($currentRowGroupCol['field']); + } + + return $builder; + + } + + public function getCurrentRowGroupCol(): ?array + { + + if (! $this->isGrouped()) { + return null; + } + + return $this->rowGroupCols[count($this->groupKeys)]; + } + + public function isColumnAvailable(string $column): bool + { + + if (! $this->isGrouped()) { + return true; + } + + return $this->getCurrentRowGroupCol()['field'] === $column; + } + + public function isGrouped(): bool + { + if (empty($this->rowGroupCols)) { + return false; + } + + // --> rowGroupCols available and not empty + + if (empty($this->groupKeys)) { + return true; + } + + // --> groupKeys available and not empty + + // Check if the all rowGroupCols are equipped with a key => no grouping anymore + return count($this->rowGroupCols) !== count($this->groupKeys); + } +} diff --git a/tests/NestedRelationFilterTest.php b/tests/NestedRelationFilterTest.php new file mode 100644 index 0000000..af7a8d5 --- /dev/null +++ b/tests/NestedRelationFilterTest.php @@ -0,0 +1,34 @@ +zooNamedVivarium = Zoo::factory()->state(['name' => 'Vivarium'])->createOne(); + $this->zooNamedOpelZoo = Zoo::factory()->state(['name' => 'Opel-Zoo'])->createOne(); + + $this->keeperNamedJohn = Keeper::factory()->for($this->zooNamedVivarium)->state(['name' => 'John'])->createOne(); + $this->keeperNamedOliver = Keeper::factory()->for($this->zooNamedOpelZoo)->state(['name' => 'Oliver'])->createOne(); + + $this->johnsFlamingos = Flamingo::factory()->count(2)->for($this->keeperNamedJohn)->create(); + $this->oliversFlamingos = Flamingo::factory()->count(3)->for($this->keeperNamedOliver)->create(); +}); + +it('handles filters on nested relations correctly', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'filterModel' => [ + 'keeper.zoo.name' => [ + 'filterType' => 'text', + 'type' => 'equals', + 'filter' => 'Vivarium', + ], + ], + ], + Flamingo::class, + ); + + expect($queryBuilder->get()->count())->toBe($this->johnsFlamingos->count()); +}); diff --git a/tests/NestedRelationJsonFilterTest.php b/tests/NestedRelationJsonFilterTest.php new file mode 100644 index 0000000..3580d25 --- /dev/null +++ b/tests/NestedRelationJsonFilterTest.php @@ -0,0 +1,44 @@ +zooNamedVivarium = Zoo::factory()->state(['name' => 'Vivarium', 'address' => [ + 'street' => 'Schnampelweg', + 'house_number' => '5', + 'postcode' => '64287', + 'city' => 'Darmstadt', + ]])->createOne(); + $this->zooNamedOpelZoo = Zoo::factory()->state(['name' => 'Opel-Zoo', 'address' => [ + 'street' => 'Am Opelzoo', + 'house_number' => '3', + 'postcode' => '61476', + 'city' => 'Kronberg im Taunus', + ]])->createOne(); + + $this->keeperNamedJohn = Keeper::factory()->for($this->zooNamedVivarium)->state(['name' => 'John'])->createOne(); + $this->keeperNamedOliver = Keeper::factory()->for($this->zooNamedOpelZoo)->state(['name' => 'Oliver'])->createOne(); + + $this->johnsFlamingos = Flamingo::factory()->count(2)->for($this->keeperNamedJohn)->create(); + $this->oliversFlamingos = Flamingo::factory()->count(3)->for($this->keeperNamedOliver)->create(); +}); + +it('handles filters on nested relations with json field correctly', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'filterModel' => [ + 'keeper.zoo.address.street' => [ + 'filterType' => 'text', + 'type' => 'equals', + 'filter' => 'Schnampelweg', + ], + ], + ], + Flamingo::class, + ); + + expect($queryBuilder->get()->count())->toBe($this->johnsFlamingos->count()); +}); diff --git a/tests/SetValuesTest.php b/tests/SetValuesTest.php new file mode 100644 index 0000000..cb9f498 --- /dev/null +++ b/tests/SetValuesTest.php @@ -0,0 +1,180 @@ +zooNamedVivarium = Zoo::factory()->state(['name' => 'Vivarium', 'address' => [ + 'street' => 'Schnampelweg', + 'house_number' => '5', + 'postcode' => '64287', + 'city' => 'Darmstadt', + ]])->createOne(); + $this->zooNamedOpelZoo = Zoo::factory()->state(['name' => 'Opel-Zoo', 'address' => [ + 'street' => 'Am Opelzoo', + 'house_number' => '3', + 'postcode' => '61476', + 'city' => 'Kronberg im Taunus', + ]])->createOne(); + + $this->keeperNamedJohn = Keeper::factory()->for($this->zooNamedVivarium)->state(['name' => 'John'])->createOne(); + $this->keeperNamedOliver = Keeper::factory()->for($this->zooNamedOpelZoo)->state(['name' => 'Oliver'])->createOne(); + + $this->johnsFlamingos = Flamingo::factory()->count(2)->for($this->keeperNamedJohn)->create(); + $this->oliversFlamingos = Flamingo::factory()->count(3)->for($this->keeperNamedOliver)->create(); +}); + +it('can retrieve set filter values for a regular column', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->pluck('name') + ->concat($this->oliversFlamingos->pluck('name')) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('can retrieve set filter values for a related column', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'keeper.name', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->name) + ->concat($this->oliversFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->name)) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('can retrieve set filter values for a nested related column', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'keeper.zoo.name', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->name) + ->concat($this->oliversFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->name)) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('can retrieve set filter values for a nested related json column field', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'keeper.zoo.address.street', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->address['street']) + ->concat($this->oliversFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->address['street'])) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('throws exception when trying to retrieve set filter values without wildcard', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(); +})->throws(UnauthorizedSetFilterColumn::class)->only(); + +it('throws exception when trying to retrieve set filter values with wrong allowed column name', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(UnauthorizedSetFilterColumn::class)->only(); + +it('applies filters when retrieving set filter values', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + 'filterModel' => [ + 'keeper.name' => [ + 'filterType' => 'text', + 'type' => 'equals', + 'filter' => 'John', + ], + ], + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->pluck('name') + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('throws exception when trying to retrieve set filter values without the column in params', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column2' => 'name', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(InvalidSetValueOperation::class)->only(); + +it('throws exception when trying to retrieve set filter with empty column in params', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => '', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(InvalidSetValueOperation::class)->only(); + +it('throws exception when trying to retrieve set filter with null column in params', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => null, + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(InvalidSetValueOperation::class)->only(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 931f0e6..7e04b1e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -38,9 +38,17 @@ protected function setUp(): void /** @var DatabaseManager $db */ $db = $this->app->get('db'); + $db->connection()->getSchemaBuilder()->create('zoos', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->jsonb('address'); + $table->timestamps(); + }); + $db->connection()->getSchemaBuilder()->create('keepers', function (Blueprint $table) { $table->id(); $table->string('name'); + $table->foreignId('zoo_id')->index()->constrained(); $table->timestamps(); }); @@ -54,7 +62,8 @@ protected function setUp(): void $table->date('last_vaccinated_on')->nullable(); $table->boolean('is_hungry')->default(false); $table->softDeletes(); - $table->foreignId('keeper_id')->constrained(); + $table->foreignId('keeper_id')->index()->constrained(); }); + } } diff --git a/tests/TestClasses/Factories/KeeperFactory.php b/tests/TestClasses/Factories/KeeperFactory.php index 04fbdbe..6e15b3d 100644 --- a/tests/TestClasses/Factories/KeeperFactory.php +++ b/tests/TestClasses/Factories/KeeperFactory.php @@ -2,6 +2,7 @@ namespace Clickbar\AgGrid\Tests\TestClasses\Factories; +use Clickbar\AgGrid\Tests\TestClasses\Models\Zoo; use Illuminate\Database\Eloquent\Factories\Factory; class KeeperFactory extends Factory @@ -10,6 +11,7 @@ public function definition(): array { return [ 'name' => $this->faker->name(), + 'zoo_id' => Zoo::factory(), ]; } } diff --git a/tests/TestClasses/Factories/ZooFactory.php b/tests/TestClasses/Factories/ZooFactory.php new file mode 100644 index 0000000..6558517 --- /dev/null +++ b/tests/TestClasses/Factories/ZooFactory.php @@ -0,0 +1,21 @@ + $this->faker->name(), + 'address' => [ + 'street' => $this->faker->streetName(), + 'house_number' => $this->faker->buildingNumber(), + 'postcode' => $this->faker->postcode(), + 'city' => $this->faker->city(), + ], + ]; + } +} diff --git a/tests/TestClasses/Models/Flamingo.php b/tests/TestClasses/Models/Flamingo.php index f835a31..de33085 100644 --- a/tests/TestClasses/Models/Flamingo.php +++ b/tests/TestClasses/Models/Flamingo.php @@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; -class Flamingo extends Model implements AgGridExportable, AgGridCustomFilterable +class Flamingo extends Model implements AgGridCustomFilterable, AgGridExportable { use HasFactory; use SoftDeletes; diff --git a/tests/TestClasses/Models/Keeper.php b/tests/TestClasses/Models/Keeper.php index ac38fb7..cfa29b7 100644 --- a/tests/TestClasses/Models/Keeper.php +++ b/tests/TestClasses/Models/Keeper.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Keeper extends Model @@ -21,4 +22,9 @@ public function flamingos(): HasMany { return $this->hasMany(Flamingo::class); } + + public function zoo(): BelongsTo + { + return $this->belongsTo(Zoo::class); + } } diff --git a/tests/TestClasses/Models/Zoo.php b/tests/TestClasses/Models/Zoo.php new file mode 100644 index 0000000..f9574bc --- /dev/null +++ b/tests/TestClasses/Models/Zoo.php @@ -0,0 +1,25 @@ + 'immutable_datetime', + 'updated_at' => 'immutable_datetime', + 'address' => 'array', + ]; + + public function keepers(): HasMany + { + return $this->hasMany(Keeper::class); + } +}