From fe628f54ffc95fbecea3de79589198ab6f976b07 Mon Sep 17 00:00:00 2001 From: Lupacescu Eduard Date: Mon, 21 Dec 2020 11:27:26 +0200 Subject: [PATCH] Sorting by belongs to. (#319) * Sorting by belongs to. * Apply fixes from StyleCI (#318) * Clean up. --- docs/docs/4.0/filtering/filtering.md | 106 ++++++++++----- src/Eager/RelatedCollection.php | 10 ++ src/Fields/EagerField.php | 25 ++++ src/Fields/Field.php | 45 ++++++- src/Filters/SortableFilter.php | 121 +++++++++++++++++- .../Search/RepositorySearchService.php | 83 +++--------- src/Sort/SortCollection.php | 78 +++++++++++ src/Traits/InteractWithSearch.php | 32 ++++- .../Actions/PerformActionsControllerTest.php | 5 +- .../RepositoryIndexControllerTest.php | 8 +- .../Feature/Filters/FilterDefinitionTest.php | 48 ++++++- tests/Feature/RepositorySearchServiceTest.php | 8 +- 12 files changed, 449 insertions(+), 120 deletions(-) create mode 100644 src/Sort/SortCollection.php diff --git a/docs/docs/4.0/filtering/filtering.md b/docs/docs/4.0/filtering/filtering.md index 20d2ee04..3745754a 100644 --- a/docs/docs/4.0/filtering/filtering.md +++ b/docs/docs/4.0/filtering/filtering.md @@ -4,7 +4,7 @@ Laravel Restify provides configurable and powerful way of filtering over entitie ## Search -If you want search for some specific fields from a model, you have to define these fields in the `$search` static +If you want search for some specific fields from a model, you have to define these fields in the `$search` static property: ```php @@ -13,8 +13,7 @@ class PostRepository extends Repository public static $search = ['id', 'title']; ``` -Now `posts` are searchable by `id` and `title`, so you could use `search` query param for filtering the index -request: +Now `posts` are searchable by `id` and `title`, so you could use `search` query param for filtering the index request: ```http request GET: /api/restify/posts?search="Test title" @@ -22,7 +21,7 @@ GET: /api/restify/posts?search="Test title" ### Get available searchables -You can use the following request to available searchable attributes for a repository: +You can use the following request to available searchable attributes for a repository: ```http request /api/restify/posts/filters?only=searchables @@ -30,7 +29,7 @@ You can use the following request to available searchable attributes for a repos ## Match -Matching by specific attributes may be useful if you want an exact matching. +Matching by specific attributes may be useful if you want an exact matching. Repository configuration: @@ -44,7 +43,7 @@ class PostRepository extends Repository } ``` -As we may notice the match configuration is an associative array, defining the attribute name and type mapping. +As we may notice the match configuration is an associative array, defining the attribute name and type mapping. Available types: @@ -68,7 +67,7 @@ GET: /api/restify/posts?title="Some title" ### Match datetime -The `datetime` filter add behind the scene an `whereDate` query. +The `datetime` filter add behind the scene an `whereDate` query. ```php class PostRepository extends Repository @@ -79,7 +78,7 @@ class PostRepository extends Repository } ``` -Request: +Request: ```http request GET: /api/restify/posts?published_at=2020-12-01 @@ -106,7 +105,7 @@ class PostRepository extends Repository } ``` -Request: +Request: ```http request GET: /api/restify/posts?id=1,2,3 @@ -128,7 +127,7 @@ GET: /api/restify/posts?-id=1,2,3 This will return all posts where doesn't have the `id` in the `[1,2,3]` list. -You can apply `-` (negation) for every match: +You can apply `-` (negation) for every match: ```http request GET: /api/restify/posts?-title="Some title" @@ -138,7 +137,9 @@ This will return all posts that doesn't contain `Some title` substring. ### Match closure -There may be situations when the filter you want to apply not necessarily is a database attributes. In your `booted` method you can add more filters for the `$match` where the key represents the field used as query param, and value should be a `Closure` which gets the request and current query `Builder`: +There may be situations when the filter you want to apply not necessarily is a database attributes. In your `booted` +method you can add more filters for the `$match` where the key represents the field used as query param, and value +should be a `Closure` which gets the request and current query `Builder`: ```php // UserRepository @@ -154,16 +155,18 @@ protected static function booted() } ``` -So now you can query this: +So now you can query this: ```http request GET: /api/restify/users?active=true ``` - ### Matchable -Sometimes you may have a large logic into a match `Closure`, and the booted method could become a mess. To prevent this, `Restify` provides a declarative way to define `matchers`. For this purpose you should define a class, and implement the `Binaryk\LaravelRestify\Repositories\Matchable` contract. You can use the following command to generate that: +Sometimes you may have a large logic into a match `Closure`, and the booted method could become a mess. To prevent +this, `Restify` provides a declarative way to define `matchers`. For this purpose you should define a class, and +implement the `Binaryk\LaravelRestify\Repositories\Matchable` contract. You can use the following command to generate +that: ```shell script php artisan restify:match ActiveMatch @@ -203,26 +206,26 @@ You can use the following request to get all repository matches: /api/restify/posts/filters?only=matches ``` -## Sort -When index query entities, usually we have to sort by specific attributes. -This requires the `$sort` configuration: +## Sort + +When index query entities, usually we have to sort by specific attributes. This requires the `$sort` configuration: ```php class PostRepository extends Repository { public static $sort = ['id']; ``` - - Performing request requires the sort query param: - - Sorting DESC requires a minus (`-`) sign before the attribute name: - + +Performing request requires the sort query param: + +Sorting DESC requires a minus (`-`) sign before the attribute name: + ```http request GET: /api/restify/posts?sort=-id ``` - Sorting ASC: - +Sorting ASC: + ```http request GET: /api/restify/posts?sort=id ``` @@ -233,22 +236,58 @@ or with plus sign before the field: GET: /api/restify/posts?sort=+id ``` +### Sort using BelongsTo + +Sometimes you may need to sort by a `belongsTo` relationship. This become a breeze with Restify. Firstly you have to +instruct your sort to use a relationship: + +```php +// PostRepository +use Binaryk\LaravelRestify\Fields\BelongsTo; +use Binaryk\LaravelRestify\Filters\SortableFilter; + +public static function sorts(): array +{ + return [ + 'users.name' => SortableFilter::make() + ->setColumn('users.name') + ->usingBelongsTo( + BelongsTo::make('user', 'user', UserRepository::class), + ) + ]; +} +``` + +Make sure that the column is fully qualified (include the table name). + +The request could look like: + +```http request +GET: /api/restify/posts?sort=-users.name +``` + +This will return all posts, sorted descending by users name. + +:::tip Set column optional +As you may notice we have typed twice the `users.name` (on the array key, and as argument in the `setColumn` method). As soon as you use the fully qualified key name, you can avoid the `setColumn` call, since the column will be injected automatically based on the `sorts` key. +::: + ### Get available sorts -You can use the following request to get sortable attributes for a repository: +You can use the following request to get sortable attributes for a repository: ```http request /api/restify/posts/filters?only=sortables ``` -:::tip All filters -You can use `/api/restify/posts/filters?only=sortables` request, and concatenate: `?only=sortables,matches, searchables` to get all of them at once. +:::tip All filters You can use `/api/restify/posts/filters?only=sortables` request, and +concatenate: `?only=sortables,matches, searchables` to get all of them at once. ::: ## Eager loading - aka withs -When get a repository index or details about a single entity, often we have to get the related entities (we have access to). -This eager loading is configurable by Restify as following: +When get a repository index or details about a single entity, often we have to get the related entities (we have access +to). This eager loading is configurable by Restify as following: ```php public static $related = ['posts']; @@ -262,8 +301,8 @@ GET: /api/restify/users?related=posts ## Custom data -You are not limited to add only relations under the `related` array. You can use whatever you want, for instance you can return a simple model, or a collection. Basically any serializable data could be added there. For example: - +You are not limited to add only relations under the `related` array. You can use whatever you want, for instance you can +return a simple model, or a collection. Basically any serializable data could be added there. For example: ```php public static $related = [ @@ -271,7 +310,7 @@ public static $related = [ ]; ``` -Then in the `Post` model we can define this method as: +Then in the `Post` model we can define this method as: ```php public function foo() { @@ -281,7 +320,8 @@ public function foo() { ### Custom data format -You can use a custom related cast class (aka transformer). You can do so by modifying the `restify.casts.related` property. The default related cast is `Binaryk\LaravelRestify\Repositories\Casts\RelatedCast`. +You can use a custom related cast class (aka transformer). You can do so by modifying the `restify.casts.related` +property. The default related cast is `Binaryk\LaravelRestify\Repositories\Casts\RelatedCast`. The cast class should extends the `Binaryk\LaravelRestify\Repositories\Casts\RepositoryCast` abstract class. diff --git a/src/Eager/RelatedCollection.php b/src/Eager/RelatedCollection.php index acd2f7bd..2baf9b82 100644 --- a/src/Eager/RelatedCollection.php +++ b/src/Eager/RelatedCollection.php @@ -2,8 +2,10 @@ namespace Binaryk\LaravelRestify\Eager; +use Binaryk\LaravelRestify\Fields\BelongsTo; use Binaryk\LaravelRestify\Fields\EagerField; use Binaryk\LaravelRestify\Fields\Field; +use Binaryk\LaravelRestify\Filters\SortableFilter; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Illuminate\Support\Collection; @@ -25,6 +27,14 @@ public function forEager(RestifyRequest $request): self ->unique('attribute'); } + public function mapIntoSortable(RestifyRequest $request): self + { + return $this->filter(fn (EagerField $field) => $field->isSortable()) + //Now we support only belongs to sort from related. + ->filter(fn (EagerField $field) => $field instanceof BelongsTo) + ->map(fn (BelongsTo $field) => SortableFilter::make()->usingBelongsTo($field)); + } + public function inRequest(RestifyRequest $request): self { return $this diff --git a/src/Fields/EagerField.php b/src/Fields/EagerField.php index 43df5305..8ed6b218 100644 --- a/src/Fields/EagerField.php +++ b/src/Fields/EagerField.php @@ -4,6 +4,8 @@ use Binaryk\LaravelRestify\Repositories\Repository; use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; @@ -64,4 +66,27 @@ public function resolve($repository, $attribute = null) return $this; } + + public function getRelation(Repository $repository): Relation + { + return $repository->resource->newQuery() + ->getRelation($this->relation); + } + + public function getRelatedModel(Repository $repository): ?Model + { + return $this->getRelation($repository)->getRelated(); + } + + public function getRelatedKey(Repository $repository): string + { + return $repository->resource->qualifyColumn( + $this->getRelation($repository)->getRelated()->getForeignKey() + ); + } + + public function getQualifiedKey(Repository $repository): string + { + return $this->getRelation($repository)->getRelated()->getQualifiedKeyName(); + } } diff --git a/src/Fields/Field.php b/src/Fields/Field.php index 88f7899c..fd42a11b 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -120,6 +120,13 @@ class Field extends OrganicField implements JsonSerializable public $label; + /** + * Indicates if the field should be sortable. + * + * @var bool + */ + public $sortable = false; + /** * Create a new field. * @@ -395,7 +402,43 @@ public function getUpdatingBulkRules(): array } /** - * Resolve the field's value for display. + * Determine if the attribute is computed. + * + * @return bool + */ + public function computed() + { + return (is_callable($this->attribute) && ! is_string($this->attribute)) || + is_callable($this->computedCallback) || $this->attribute == 'Computed'; + } + + /** + * Specify that this attribute should be sortable. + * + * @param bool $value + * @return $this + */ + public function sortable($value = true) + { + if (! $this->computed()) { + $this->sortable = $value; + } + + return $this; + } + + /** + * Indicates if this attribute is sortable. + * + * @return bool + */ + public function isSortable() + { + return $this->sortable; + } + + /** + * Resolve the attribute's value for display. * * @param mixed $repository * @param string|null $attribute diff --git a/src/Filters/SortableFilter.php b/src/Filters/SortableFilter.php index 3ec1316e..9d378a21 100644 --- a/src/Filters/SortableFilter.php +++ b/src/Filters/SortableFilter.php @@ -2,19 +2,130 @@ namespace Binaryk\LaravelRestify\Filters; +use Binaryk\LaravelRestify\Fields\BelongsTo; +use Binaryk\LaravelRestify\Fields\EagerField; use Binaryk\LaravelRestify\Filter; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; +use Closure; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Str; class SortableFilter extends Filter { public static $uriKey = 'sortables'; + public string $direction = 'asc'; + + private BelongsTo $belongsToField; + + private Closure $resolver; + + /** + * @param RestifyRequest $request + * @param Builder $query + * @param $direction + * @return false|mixed + */ public function filter(RestifyRequest $request, $query, $direction) { - $query->orderBy($this->column, - $direction === '-' - ? 'desc' - : 'asc' - ); + if (isset($this->resolver) && is_callable($this->resolver)) { + return call_user_func($this->resolver, $request, $query, $direction); + } + + if (isset($this->belongsToField)) { + if (! $this->belongsToField->authorize($request)) { + return $query; + } + + // This approach could be rewritten using join. + $query->orderBy($this->belongsToField->getRelatedModel($this->repository)::select('name') + ->whereColumn( + $this->belongsToField->getQualifiedKey($this->repository), + $this->belongsToField->getRelatedKey($this->repository)) + ->orderBy($this->getColumn(), $direction) + ->take(1), + $direction + ); + + return $query; + } + + $query->orderBy($this->column, $direction); + } + + public function usingBelongsTo(BelongsTo $field) + { + $this->belongsToField = $field; + +// $this->setColumn($field->attam ) //todo + + return $this; + } + + public function getEager(): ?EagerField + { + if (! isset($this->belongsToField)) { + return null; + } + + return $this->belongsToField; + } + + public function hasEager(): bool + { + return isset($this->belongsToField) && $this->belongsToField instanceof EagerField; + } + + public function asc(): self + { + $this->direction = 'asc'; + + return $this; + } + + public function desc(): self + { + $this->direction = 'desc'; + + return $this; + } + + public function direction(): string + { + return $this->direction; + } + + public function syncDirection(string $direction = null): self + { + if (! is_null($direction) && in_array($direction, ['asc', 'desc'])) { + $this->direction = $direction; + + return $this; + } + + if (Str::startsWith($this->column, '-')) { + $this->desc(); + + $this->column = Str::after($this->column, '-'); + + return $this; + } + + if (Str::startsWith($this->column, '+')) { + $this->asc(); + + $this->column = Str::after($this->column, '+'); + + return $this; + } + + return $this->asc(); + } + + public function usingClosure(Closure $closure): self + { + $this->resolver = $closure; + + return $this; } } diff --git a/src/Services/Search/RepositorySearchService.php b/src/Services/Search/RepositorySearchService.php index 4edc0ac3..404b08ba 100644 --- a/src/Services/Search/RepositorySearchService.php +++ b/src/Services/Search/RepositorySearchService.php @@ -14,6 +14,9 @@ class RepositorySearchService extends Searchable { + /** + * @var Repository + */ protected $repository; public function search(RestifyRequest $request, Repository $repository) @@ -33,7 +36,7 @@ public function prepareMatchFields(RestifyRequest $request, $query, $extra = []) { /** * @var Builder $query */ $model = $query->getModel(); - foreach ($this->repository->getMatchByFields($request) as $key => $type) { + foreach ($this->repository->getMatchByFields() as $key => $type) { $negation = false; if ($request->has('-'.$key)) { @@ -91,25 +94,26 @@ public function prepareMatchFields(RestifyRequest $request, $query, $extra = []) return $query; } - public function prepareOrders(RestifyRequest $request, $query, $extra = []) + /** + * Resolve orders. + * + * @param RestifyRequest $request + * @param Builder $query + * @return Builder + */ + public function prepareOrders(RestifyRequest $request, $query) { - $orderings = explode(',', $request->input('sort', '')); + $collection = $this->repository::collectSorts($request, $this->repository); - if (isset($extra['sort'])) { - $orderings = $extra['sort']; + if ($collection->isEmpty()) { + return empty($query->getQuery()->orders) + ? $query->latest($query->getModel()->getQualifiedKeyName()) + : $query; } - $params = array_filter($orderings); - - if (is_array($params) === true && empty($params) === false) { - foreach ($params as $param) { - $this->setOrder($request, $query, $param); - } - } - - if (empty($params) === true) { - $this->setOrder($request, $query, '+'.$this->repository->newModel()->getKeyName()); - } + $collection->each(function (SortableFilter $filter) use ($request, $query) { + $filter->filter($request, $query, $filter->direction()); + }); return $query; } @@ -169,53 +173,6 @@ public function prepareSearchFields(RestifyRequest $request, $query, $extra = [] return $query; } - public function setOrder(RestifyRequest $request, $query, $param) - { - if ($param === 'random') { - $query->inRandomOrder(); - - return $query; - } - - $order = substr($param, 0, 1); - - if ($order === '-') { - $field = substr($param, 1); - } - - if ($order === '+') { - $field = substr($param, 1); - } - - if ($order !== '-' && $order !== '+') { - $order = '+'; - $field = $param; - } - - $field = $field ?? $this->repository->newModel()->getKeyName(); - - if (isset($field)) { - foreach ($this->repository->getOrderByFields() as $column => $definitionField) { - $filter = $definitionField instanceof Filter - ? $definitionField - : SortableFilter::make(); - - $filter->setRepository($this->repository) - ->setColumn( - $filter->column ?? $query->qualifyColumn($field) - ); - - $filter->filter($request, $query, $order); - } - - if ($field === 'random') { - $query->orderByRaw('RAND()'); - } - } - - return $query; - } - protected function applyIndexQuery(RestifyRequest $request, Repository $repository) { return fn ($query) => $repository::indexQuery($request, $query); diff --git a/src/Sort/SortCollection.php b/src/Sort/SortCollection.php new file mode 100644 index 00000000..8b458876 --- /dev/null +++ b/src/Sort/SortCollection.php @@ -0,0 +1,78 @@ + $item) { + $queryKey = is_numeric($key) ? $item : $key; + $definition = $item instanceof Filter + ? $item + : SortableFilter::make(); + + $definition->setColumn( + $definition->column ?? $queryKey + ); + + $unified[] = $definition; + } + + parent::__construct($unified); + } + + public function hydrateRepository(Repository $repository): self + { + return $this->map(fn (Filter $filter) => $filter->setRepository($repository)); + } + + public function allowed(RestifyRequest $request, Repository $repository) + { + $collection = static::make($repository::getOrderByFields()); + + return $this->filter(fn (SortableFilter $filter) => $collection->contains('column', '=', $filter->column)); + } + + public function hydrateDefinition(Repository $repository): SortCollection + { + return $this->map(function (SortableFilter $filter) use ($repository) { + if (! array_key_exists($filter->column, $repository::getOrderByFields())) { + return $filter; + } + + $definition = Arr::get($repository::getOrderByFields(), $filter->getColumn()); + + if (is_callable($definition)) { + return $filter->usingClosure($definition); + } + + if ($definition instanceof SortableFilter) { + return $definition->syncDirection($filter->direction()); + } + + throw new Exception("Invalid argument to {$filter->column} sort in repository."); + }); + } + + public function forEager(RestifyRequest $request): self + { + return $this->filter(fn (SortableFilter $filter) => $filter->hasEager()) + ->unique('column'); + } + + public function normalize() + { + return $this->map(fn (SortableFilter $filter) => $filter->syncDirection()); + } +} diff --git a/src/Traits/InteractWithSearch.php b/src/Traits/InteractWithSearch.php index cd925fc5..ac893239 100644 --- a/src/Traits/InteractWithSearch.php +++ b/src/Traits/InteractWithSearch.php @@ -7,11 +7,11 @@ use Binaryk\LaravelRestify\Filters\MatchFilter; use Binaryk\LaravelRestify\Filters\SearchableFilter; use Binaryk\LaravelRestify\Filters\SortableFilter; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; +use Binaryk\LaravelRestify\Repositories\Repository; +use Binaryk\LaravelRestify\Sort\SortCollection; use Illuminate\Support\Collection; -/** - * @author Eduard Lupacescu - */ trait InteractWithSearch { use AuthorizableModels; @@ -27,18 +27,23 @@ public static function getSearchableFields() : static::$search; } - /** - * @return array - */ public static function getWiths() { return static::$withs ?? []; } /** + * Use 'related' instead. + * * @return array + * @deprecated */ public static function getRelated() + { + return static::related(); + } + + public static function related(): array { return static::$related ?? []; } @@ -60,14 +65,29 @@ public static function getMatchByFields() /** * @return array + * @deprecated */ public static function getOrderByFields() + { + return static::sorts(); + } + + public static function sorts(): array { return empty(static::$sort) ? [static::newModel()->getQualifiedKeyName()] : static::$sort; } + public static function collectSorts(RestifyRequest $request, Repository $repository): SortCollection + { + return SortCollection::make(explode(',', $request->input('sort', ''))) + ->normalize() + ->allowed($request, $repository) + ->hydrateDefinition($repository) + ->hydrateRepository($repository); + } + public static function collectFilters($type): Collection { $filters = collect([ diff --git a/tests/Actions/PerformActionsControllerTest.php b/tests/Actions/PerformActionsControllerTest.php index 7e6d943f..11923874 100644 --- a/tests/Actions/PerformActionsControllerTest.php +++ b/tests/Actions/PerformActionsControllerTest.php @@ -30,8 +30,9 @@ public function test_could_perform_action_for_multiple_repositories() 'data', ]); - $this->assertEquals(1, PublishPostAction::$applied[0][0]->id); - $this->assertEquals(2, PublishPostAction::$applied[0][1]->id); + // Repositories are sorted desc by primary key. + $this->assertEquals(2, PublishPostAction::$applied[0][0]->id); + $this->assertEquals(1, PublishPostAction::$applied[0][1]->id); } public function test_cannot_apply_a_show_action_to_index() diff --git a/tests/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php index 4503630c..3c7c1a3b 100644 --- a/tests/Controllers/RepositoryIndexControllerTest.php +++ b/tests/Controllers/RepositoryIndexControllerTest.php @@ -65,19 +65,17 @@ public function test_repository_order() factory(Post::class)->create(['title' => 'zzz']); - $response = $this - ->getJson('posts?sort=-title') + $response = $this->getJson('posts?sort=-title') ->assertOk(); $this->assertEquals('zzz', $response->json('data.0.attributes.title')); $this->assertEquals('aaa', $response->json('data.1.attributes.title')); - $response = $this - ->getJson('posts?order=-title') + $response = $this->getJson('posts?sort=title') ->assertOk(); - $this->assertEquals('zzz', $response->json('data.1.attributes.title')); $this->assertEquals('aaa', $response->json('data.0.attributes.title')); + $this->assertEquals('zzz', $response->json('data.1.attributes.title')); } public function test_repository_with_relations() diff --git a/tests/Feature/Filters/FilterDefinitionTest.php b/tests/Feature/Filters/FilterDefinitionTest.php index cba6e8d4..32653b9f 100644 --- a/tests/Feature/Filters/FilterDefinitionTest.php +++ b/tests/Feature/Filters/FilterDefinitionTest.php @@ -2,12 +2,17 @@ namespace Binaryk\LaravelRestify\Tests\Feature\Filters; +use Binaryk\LaravelRestify\Fields\BelongsTo; use Binaryk\LaravelRestify\Filters\MatchFilter; use Binaryk\LaravelRestify\Filters\SearchableFilter; use Binaryk\LaravelRestify\Filters\SortableFilter; +use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; +use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post; use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository; +use Binaryk\LaravelRestify\Tests\Fixtures\User\User; use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; +use Illuminate\Database\Eloquent\Builder; class FilterDefinitionTest extends IntegrationTest { @@ -46,7 +51,6 @@ public function test_match_definitions_includes_title() ]; $this->getJson('posts/filters?only=matches') - ->dump() ->assertJsonStructure([ 'data' => [ [ @@ -60,4 +64,46 @@ public function test_match_definitions_includes_title() ], ]); } + + public function test_can_filter_using_belongs_to_field() + { + PostRepository::$related = [ + 'user' => BelongsTo::make('user', 'user', UserRepository::class), + ]; + + PostRepository::$sort = [ + 'users.name' => SortableFilter::make()->usingBelongsTo( + BelongsTo::make('user', 'user', UserRepository::class), + ) + /*function (RestifyRequest $request, Builder $builder, $direction) { + $builder->join('users', 'posts.user_id', '=', 'users.id') + ->select('posts.*') + ->orderBy('users.name', $direction); + }*/, + ]; + + factory(Post::class)->create([ + 'user_id' => factory(User::class)->create([ + 'name' => 'Zez', + ]), + ]); + + factory(Post::class)->create([ + 'user_id' => factory(User::class)->create([ + 'name' => 'Ame', + ]), + ]); + + $json = $this->getJson(PostRepository::uriKey().'?related=user&sort=-users.name')->json(); + + $this->assertSame( + 'Zez', + data_get($json, 'data.0.relationships.user.attributes.name') + ); + + $this->assertSame( + 'Ame', + data_get($json, 'data.1.relationships.user.attributes.name') + ); + } } diff --git a/tests/Feature/RepositorySearchServiceTest.php b/tests/Feature/RepositorySearchServiceTest.php index 7d23ad3f..cc53027c 100644 --- a/tests/Feature/RepositorySearchServiceTest.php +++ b/tests/Feature/RepositorySearchServiceTest.php @@ -107,12 +107,12 @@ public function test_can_order_using_filter_sortable_definition() ]; $this->assertSame('Alisa', $this->getJson('users?sort=name') - ->json('data.0.attributes.name')); - $this->assertSame('Zoro', $this->getJson('users?sort=name') - ->json('data.1.attributes.name')); + ->json('data.0.attributes.name')); + $this->assertSame('Zoro', $this->getJson('users?sort=name') + ->json('data.1.attributes.name')); $this->assertSame('Zoro', $this->getJson('users?sort=-name') - ->json('data.0.attributes.name')); + ->json('data.0.attributes.name')); $this->assertSame('Alisa', $this->getJson('users?sort=-name') ->json('data.1.attributes.name')); }