From f57f76afcaf764d0468e0ebeeebb81e7157a2ece Mon Sep 17 00:00:00 2001 From: Gautier DELEGLISE Date: Mon, 11 Aug 2025 22:07:38 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20scout=20with=20and=20only=20tra?= =?UTF-8?q?shed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Query/ScoutBuilder.php | 13 ++++ src/Rules/Search/SearchText.php | 4 + .../Controllers/SearchScoutOperationsTest.php | 76 +++++++++++++++++++ tests/Unit/LaravelScoutTest.php | 60 ++++++++++++--- 4 files changed, 144 insertions(+), 9 deletions(-) diff --git a/src/Query/ScoutBuilder.php b/src/Query/ScoutBuilder.php index 5d375dc..df6e871 100644 --- a/src/Query/ScoutBuilder.php +++ b/src/Query/ScoutBuilder.php @@ -41,6 +41,10 @@ public function search(array $parameters = []) $this->applyFilters($parameters['filters']); }); + $this->when(isset($parameters['text']['trashed']), function () use ($parameters) { + $this->applyTrashed($parameters['text']['trashed']); + }); + $this->when(isset($parameters['sorts']), function () use ($parameters) { $this->applySorts($parameters['sorts']); }); @@ -125,6 +129,15 @@ public function applySorts($sorts) } } + public function applyTrashed(string $trashed): void + { + if ($trashed === 'only') { + $this->queryBuilder->onlyTrashed(); + } elseif ($trashed === 'with') { + $this->queryBuilder->withTrashed(); + } + } + /** * Apply an instruction to the query builder. * diff --git a/src/Rules/Search/SearchText.php b/src/Rules/Search/SearchText.php index 441d420..5859e52 100644 --- a/src/Rules/Search/SearchText.php +++ b/src/Rules/Search/SearchText.php @@ -2,6 +2,7 @@ namespace Lomkit\Rest\Rules\Search; +use Illuminate\Validation\Rule; use Lomkit\Rest\Rules\RestRule; class SearchText extends RestRule @@ -17,6 +18,9 @@ public function buildValidationRules(string $attribute, mixed $value): array return [ $attribute => ['sometimes', 'array'], $attribute.'.value' => ['nullable', 'string'], + $attribute.'.trashed' => [ + Rule::in('with', 'only'), + ], ]; } } diff --git a/tests/Feature/Controllers/SearchScoutOperationsTest.php b/tests/Feature/Controllers/SearchScoutOperationsTest.php index 7d4b3d3..6a54c6b 100644 --- a/tests/Feature/Controllers/SearchScoutOperationsTest.php +++ b/tests/Feature/Controllers/SearchScoutOperationsTest.php @@ -4,11 +4,13 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Gate; +use Lomkit\Rest\Query\ScoutBuilder; use Lomkit\Rest\Tests\Feature\TestCase; use Lomkit\Rest\Tests\Support\Database\Factories\ModelFactory; use Lomkit\Rest\Tests\Support\Models\Model; use Lomkit\Rest\Tests\Support\Policies\GreenPolicy; use Lomkit\Rest\Tests\Support\Rest\Resources\ModelResource; +use Mockery\MockInterface; class SearchScoutOperationsTest extends TestCase { @@ -379,4 +381,78 @@ public function test_getting_a_list_of_resources_with_null_scout(): void new ModelResource() ); } + + public function test_getting_a_list_of_resources_with_unknown_trashed(): void + { + ModelFactory::new()->count(2)->create(); + + Gate::policy(Model::class, GreenPolicy::class); + + $response = $this->post( + '/api/searchable-models/search', + [ + 'search' => [ + 'text' => [ + 'trashed' => 'unknown', + ], + ], + ], + ['Accept' => 'application/json'] + ); + + $response->assertStatus(422); + $response->assertExactJsonStructure(['message', 'errors' => ['search.text.trashed']]); + } + + public function test_getting_a_list_of_resources_with_trashed(): void + { + ModelFactory::new()->count(2)->create(); + + Gate::policy(Model::class, GreenPolicy::class); + + $response = $this->post( + '/api/searchable-models/search', + [ + 'search' => [ + 'text' => [ + 'trashed' => 'with', + ], + ], + ], + ['Accept' => 'application/json'] + ); + + $this->assertResourcePaginated( + $response, + [], + new ModelResource() + ); + } + + public function test_getting_a_list_of_resources_only_trashed(): void + { + ModelFactory::new()->count(2)->create(); + + Gate::policy(Model::class, GreenPolicy::class); + + $response = $this->post( + '/api/searchable-models/search', + [ + 'search' => [ + 'text' => [ + 'trashed' => 'only', + ], + ], + ], + ['Accept' => 'application/json'] + ); + + // @TODO: test only correctly applyed + + $this->assertResourcePaginated( + $response, + [], + new ModelResource() + ); + } } diff --git a/tests/Unit/LaravelScoutTest.php b/tests/Unit/LaravelScoutTest.php index 45b5d44..ea377cb 100644 --- a/tests/Unit/LaravelScoutTest.php +++ b/tests/Unit/LaravelScoutTest.php @@ -47,9 +47,7 @@ public function test_building_scout_with_filters() $scoutQueryBuilderMock ->search([ - 'text' => [ - ['value' => 'test'], - ], + 'text' => ['value' => 'test'], 'filters' => [ ['field' => 'test', 'value' => 1], ], @@ -69,9 +67,7 @@ public function test_building_scout_with_sorts() $scoutQueryBuilderMock ->search([ - 'text' => [ - ['value' => 'test'], - ], + 'text' => ['value' => 'test'], 'sorts' => [ ['field' => 'id'], ], @@ -91,9 +87,7 @@ public function test_building_scout_with_instructions() $scoutQueryBuilderMock ->search([ - 'text' => [ - ['value' => 'test'], - ], + 'text' => ['value' => 'test'], 'instructions' => [ ['name' => 'my_instruction'], ], @@ -101,4 +95,52 @@ public function test_building_scout_with_instructions() ($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query()); } + + public function test_building_scout_with_trashed() + { + Auth::setUser(Mockery::mock(User::class)); + Gate::policy(Model::class, GreenPolicy::class); + + $scoutQueryBuilderMock = Mockery::mock(ScoutBuilder::class, [new SearchableModelResource()])->makePartial(); + + $scoutQueryBuilderMock->shouldReceive('applyFilters')->never(); + $scoutQueryBuilderMock->shouldReceive('applySorts')->never(); + $scoutQueryBuilderMock->shouldReceive('applyInstructions')->never(); + $scoutQueryBuilderMock->shouldReceive('applyTrashed')->with('with')->once(); + + $scoutQueryBuilderMock + ->search([ + 'text' => + [ + 'value' => 'test', + 'trashed' => 'with' + ], + ]); + + ($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query()); + } + + public function test_building_scout_only_trashed() + { + Auth::setUser(Mockery::mock(User::class)); + Gate::policy(Model::class, GreenPolicy::class); + + $scoutQueryBuilderMock = Mockery::mock(ScoutBuilder::class, [new SearchableModelResource()])->makePartial(); + + $scoutQueryBuilderMock->shouldReceive('applyFilters')->never(); + $scoutQueryBuilderMock->shouldReceive('applySorts')->never(); + $scoutQueryBuilderMock->shouldReceive('applyInstructions')->never(); + $scoutQueryBuilderMock->shouldReceive('applyTrashed')->with('only')->once(); + + $scoutQueryBuilderMock + ->search([ + 'text' => + [ + 'value' => 'test', + 'trashed' => 'only' + ], + ]); + + ($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query()); + } } From 488e993f039ba3e4f528f55778ad1ab09375d963 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 11 Aug 2025 20:07:50 +0000 Subject: [PATCH 2/5] Apply fixes from StyleCI --- src/Rules/Search/SearchText.php | 4 ++-- .../Controllers/SearchScoutOperationsTest.php | 2 -- tests/Unit/LaravelScoutTest.php | 24 +++++++++---------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Rules/Search/SearchText.php b/src/Rules/Search/SearchText.php index 5859e52..f913a41 100644 --- a/src/Rules/Search/SearchText.php +++ b/src/Rules/Search/SearchText.php @@ -16,8 +16,8 @@ public function buildValidationRules(string $attribute, mixed $value): array } return [ - $attribute => ['sometimes', 'array'], - $attribute.'.value' => ['nullable', 'string'], + $attribute => ['sometimes', 'array'], + $attribute.'.value' => ['nullable', 'string'], $attribute.'.trashed' => [ Rule::in('with', 'only'), ], diff --git a/tests/Feature/Controllers/SearchScoutOperationsTest.php b/tests/Feature/Controllers/SearchScoutOperationsTest.php index 6a54c6b..09c5da4 100644 --- a/tests/Feature/Controllers/SearchScoutOperationsTest.php +++ b/tests/Feature/Controllers/SearchScoutOperationsTest.php @@ -4,13 +4,11 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Gate; -use Lomkit\Rest\Query\ScoutBuilder; use Lomkit\Rest\Tests\Feature\TestCase; use Lomkit\Rest\Tests\Support\Database\Factories\ModelFactory; use Lomkit\Rest\Tests\Support\Models\Model; use Lomkit\Rest\Tests\Support\Policies\GreenPolicy; use Lomkit\Rest\Tests\Support\Rest\Resources\ModelResource; -use Mockery\MockInterface; class SearchScoutOperationsTest extends TestCase { diff --git a/tests/Unit/LaravelScoutTest.php b/tests/Unit/LaravelScoutTest.php index ea377cb..397c271 100644 --- a/tests/Unit/LaravelScoutTest.php +++ b/tests/Unit/LaravelScoutTest.php @@ -47,7 +47,7 @@ public function test_building_scout_with_filters() $scoutQueryBuilderMock ->search([ - 'text' => ['value' => 'test'], + 'text' => ['value' => 'test'], 'filters' => [ ['field' => 'test', 'value' => 1], ], @@ -67,7 +67,7 @@ public function test_building_scout_with_sorts() $scoutQueryBuilderMock ->search([ - 'text' => ['value' => 'test'], + 'text' => ['value' => 'test'], 'sorts' => [ ['field' => 'id'], ], @@ -87,7 +87,7 @@ public function test_building_scout_with_instructions() $scoutQueryBuilderMock ->search([ - 'text' => ['value' => 'test'], + 'text' => ['value' => 'test'], 'instructions' => [ ['name' => 'my_instruction'], ], @@ -110,11 +110,10 @@ public function test_building_scout_with_trashed() $scoutQueryBuilderMock ->search([ - 'text' => - [ - 'value' => 'test', - 'trashed' => 'with' - ], + 'text' => [ + 'value' => 'test', + 'trashed' => 'with', + ], ]); ($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query()); @@ -134,11 +133,10 @@ public function test_building_scout_only_trashed() $scoutQueryBuilderMock ->search([ - 'text' => - [ - 'value' => 'test', - 'trashed' => 'only' - ], + 'text' => [ + 'value' => 'test', + 'trashed' => 'only', + ], ]); ($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query()); From c0aa29dcab294dbee4a0c01211ebd47e867e43f3 Mon Sep 17 00:00:00 2001 From: Gautier DELEGLISE Date: Mon, 11 Aug 2025 22:17:11 +0200 Subject: [PATCH 3/5] ai fixes --- src/Query/ScoutBuilder.php | 2 +- tests/Feature/Controllers/SearchScoutOperationsTest.php | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Query/ScoutBuilder.php b/src/Query/ScoutBuilder.php index df6e871..0621a3f 100644 --- a/src/Query/ScoutBuilder.php +++ b/src/Query/ScoutBuilder.php @@ -23,7 +23,7 @@ class ScoutBuilder implements QueryBuilder * underlying search operation. * * @param array $parameters An associative array of search criteria, which may include: - * - 'text': The search query string. + * - 'text': An array containing 'value' (search string) and optionally 'trashed' ('with'|'only'). * - 'filters': An array of filter conditions. * - 'sorts': An array of sorting directives. * - 'instructions': Additional query instructions. diff --git a/tests/Feature/Controllers/SearchScoutOperationsTest.php b/tests/Feature/Controllers/SearchScoutOperationsTest.php index 09c5da4..993c19f 100644 --- a/tests/Feature/Controllers/SearchScoutOperationsTest.php +++ b/tests/Feature/Controllers/SearchScoutOperationsTest.php @@ -444,9 +444,7 @@ public function test_getting_a_list_of_resources_only_trashed(): void ], ['Accept' => 'application/json'] ); - - // @TODO: test only correctly applyed - + $this->assertResourcePaginated( $response, [], From 9284c49aa443eceb0809dac5478fde59b3e10613 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 11 Aug 2025 20:17:18 +0000 Subject: [PATCH 4/5] Apply fixes from StyleCI --- tests/Feature/Controllers/SearchScoutOperationsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Controllers/SearchScoutOperationsTest.php b/tests/Feature/Controllers/SearchScoutOperationsTest.php index 993c19f..c4bf68f 100644 --- a/tests/Feature/Controllers/SearchScoutOperationsTest.php +++ b/tests/Feature/Controllers/SearchScoutOperationsTest.php @@ -444,7 +444,7 @@ public function test_getting_a_list_of_resources_only_trashed(): void ], ['Accept' => 'application/json'] ); - + $this->assertResourcePaginated( $response, [], From abdce7c0fda37d2be9746e89633b0b86075b026b Mon Sep 17 00:00:00 2001 From: Gautier DELEGLISE Date: Mon, 11 Aug 2025 22:29:35 +0200 Subject: [PATCH 5/5] Update ScoutBuilder.php --- src/Query/ScoutBuilder.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Query/ScoutBuilder.php b/src/Query/ScoutBuilder.php index 0621a3f..9b54f35 100644 --- a/src/Query/ScoutBuilder.php +++ b/src/Query/ScoutBuilder.php @@ -129,6 +129,15 @@ public function applySorts($sorts) } } + /** + * Apply soft-delete visibility to the underlying Scout query builder. + * + * Sets the query to include only soft-deleted records when `$trashed` is `"only"`, + * or to include both deleted and non-deleted records when `$trashed` is `"with"`. + * Any other value is ignored (no change). + * + * @param string $trashed One of: "only" (only trashed), "with" (include trashed), or other (no-op). + */ public function applyTrashed(string $trashed): void { if ($trashed === 'only') {