Skip to content

Commit 394f6e9

Browse files
authored
✨ scout with and only trashed (#186)
1 parent acf8050 commit 394f6e9

File tree

4 files changed

+150
-12
lines changed

4 files changed

+150
-12
lines changed

src/Query/ScoutBuilder.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class ScoutBuilder implements QueryBuilder
2323
* underlying search operation.
2424
*
2525
* @param array $parameters An associative array of search criteria, which may include:
26-
* - 'text': The search query string.
26+
* - 'text': An array containing 'value' (search string) and optionally 'trashed' ('with'|'only').
2727
* - 'filters': An array of filter conditions.
2828
* - 'sorts': An array of sorting directives.
2929
* - 'instructions': Additional query instructions.
@@ -41,6 +41,10 @@ public function search(array $parameters = [])
4141
$this->applyFilters($parameters['filters']);
4242
});
4343

44+
$this->when(isset($parameters['text']['trashed']), function () use ($parameters) {
45+
$this->applyTrashed($parameters['text']['trashed']);
46+
});
47+
4448
$this->when(isset($parameters['sorts']), function () use ($parameters) {
4549
$this->applySorts($parameters['sorts']);
4650
});
@@ -125,6 +129,24 @@ public function applySorts($sorts)
125129
}
126130
}
127131

132+
/**
133+
* Apply soft-delete visibility to the underlying Scout query builder.
134+
*
135+
* Sets the query to include only soft-deleted records when `$trashed` is `"only"`,
136+
* or to include both deleted and non-deleted records when `$trashed` is `"with"`.
137+
* Any other value is ignored (no change).
138+
*
139+
* @param string $trashed One of: "only" (only trashed), "with" (include trashed), or other (no-op).
140+
*/
141+
public function applyTrashed(string $trashed): void
142+
{
143+
if ($trashed === 'only') {
144+
$this->queryBuilder->onlyTrashed();
145+
} elseif ($trashed === 'with') {
146+
$this->queryBuilder->withTrashed();
147+
}
148+
}
149+
128150
/**
129151
* Apply an instruction to the query builder.
130152
*

src/Rules/Search/SearchText.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Lomkit\Rest\Rules\Search;
44

5+
use Illuminate\Validation\Rule;
56
use Lomkit\Rest\Rules\RestRule;
67

78
class SearchText extends RestRule
@@ -15,8 +16,11 @@ public function buildValidationRules(string $attribute, mixed $value): array
1516
}
1617

1718
return [
18-
$attribute => ['sometimes', 'array'],
19-
$attribute.'.value' => ['nullable', 'string'],
19+
$attribute => ['sometimes', 'array'],
20+
$attribute.'.value' => ['nullable', 'string'],
21+
$attribute.'.trashed' => [
22+
Rule::in('with', 'only'),
23+
],
2024
];
2125
}
2226
}

tests/Feature/Controllers/SearchScoutOperationsTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,76 @@ public function test_getting_a_list_of_resources_with_null_scout(): void
379379
new ModelResource()
380380
);
381381
}
382+
383+
public function test_getting_a_list_of_resources_with_unknown_trashed(): void
384+
{
385+
ModelFactory::new()->count(2)->create();
386+
387+
Gate::policy(Model::class, GreenPolicy::class);
388+
389+
$response = $this->post(
390+
'/api/searchable-models/search',
391+
[
392+
'search' => [
393+
'text' => [
394+
'trashed' => 'unknown',
395+
],
396+
],
397+
],
398+
['Accept' => 'application/json']
399+
);
400+
401+
$response->assertStatus(422);
402+
$response->assertExactJsonStructure(['message', 'errors' => ['search.text.trashed']]);
403+
}
404+
405+
public function test_getting_a_list_of_resources_with_trashed(): void
406+
{
407+
ModelFactory::new()->count(2)->create();
408+
409+
Gate::policy(Model::class, GreenPolicy::class);
410+
411+
$response = $this->post(
412+
'/api/searchable-models/search',
413+
[
414+
'search' => [
415+
'text' => [
416+
'trashed' => 'with',
417+
],
418+
],
419+
],
420+
['Accept' => 'application/json']
421+
);
422+
423+
$this->assertResourcePaginated(
424+
$response,
425+
[],
426+
new ModelResource()
427+
);
428+
}
429+
430+
public function test_getting_a_list_of_resources_only_trashed(): void
431+
{
432+
ModelFactory::new()->count(2)->create();
433+
434+
Gate::policy(Model::class, GreenPolicy::class);
435+
436+
$response = $this->post(
437+
'/api/searchable-models/search',
438+
[
439+
'search' => [
440+
'text' => [
441+
'trashed' => 'only',
442+
],
443+
],
444+
],
445+
['Accept' => 'application/json']
446+
);
447+
448+
$this->assertResourcePaginated(
449+
$response,
450+
[],
451+
new ModelResource()
452+
);
453+
}
382454
}

tests/Unit/LaravelScoutTest.php

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ public function test_building_scout_with_filters()
4747

4848
$scoutQueryBuilderMock
4949
->search([
50-
'text' => [
51-
['value' => 'test'],
52-
],
50+
'text' => ['value' => 'test'],
5351
'filters' => [
5452
['field' => 'test', 'value' => 1],
5553
],
@@ -69,9 +67,7 @@ public function test_building_scout_with_sorts()
6967

7068
$scoutQueryBuilderMock
7169
->search([
72-
'text' => [
73-
['value' => 'test'],
74-
],
70+
'text' => ['value' => 'test'],
7571
'sorts' => [
7672
['field' => 'id'],
7773
],
@@ -91,14 +87,58 @@ public function test_building_scout_with_instructions()
9187

9288
$scoutQueryBuilderMock
9389
->search([
94-
'text' => [
95-
['value' => 'test'],
96-
],
90+
'text' => ['value' => 'test'],
9791
'instructions' => [
9892
['name' => 'my_instruction'],
9993
],
10094
]);
10195

10296
($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query());
10397
}
98+
99+
public function test_building_scout_with_trashed()
100+
{
101+
Auth::setUser(Mockery::mock(User::class));
102+
Gate::policy(Model::class, GreenPolicy::class);
103+
104+
$scoutQueryBuilderMock = Mockery::mock(ScoutBuilder::class, [new SearchableModelResource()])->makePartial();
105+
106+
$scoutQueryBuilderMock->shouldReceive('applyFilters')->never();
107+
$scoutQueryBuilderMock->shouldReceive('applySorts')->never();
108+
$scoutQueryBuilderMock->shouldReceive('applyInstructions')->never();
109+
$scoutQueryBuilderMock->shouldReceive('applyTrashed')->with('with')->once();
110+
111+
$scoutQueryBuilderMock
112+
->search([
113+
'text' => [
114+
'value' => 'test',
115+
'trashed' => 'with',
116+
],
117+
]);
118+
119+
($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query());
120+
}
121+
122+
public function test_building_scout_only_trashed()
123+
{
124+
Auth::setUser(Mockery::mock(User::class));
125+
Gate::policy(Model::class, GreenPolicy::class);
126+
127+
$scoutQueryBuilderMock = Mockery::mock(ScoutBuilder::class, [new SearchableModelResource()])->makePartial();
128+
129+
$scoutQueryBuilderMock->shouldReceive('applyFilters')->never();
130+
$scoutQueryBuilderMock->shouldReceive('applySorts')->never();
131+
$scoutQueryBuilderMock->shouldReceive('applyInstructions')->never();
132+
$scoutQueryBuilderMock->shouldReceive('applyTrashed')->with('only')->once();
133+
134+
$scoutQueryBuilderMock
135+
->search([
136+
'text' => [
137+
'value' => 'test',
138+
'trashed' => 'only',
139+
],
140+
]);
141+
142+
($scoutQueryBuilderMock->toBase()->queryCallback)(Model::query());
143+
}
104144
}

0 commit comments

Comments
 (0)