Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/Http/Requests/Rest/SearchRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Http\Requests\Rest;

use App\Rules\Search\Search;
use Lomkit\Rest\Http\Requests\SearchRequest as LomkitSearchRequest;

class SearchRequest extends LomkitSearchRequest
{
public function rules(): array
{
$resource = $this->route()->controller::newResource();

return [
'search' => (new Search)->setResource($resource),
];
}
}
7 changes: 6 additions & 1 deletion app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Providers;

use App\Http\Requests\Rest\SearchRequest as AppSearchRequest;
use App\Models\Citation;
use App\Models\Collection;
use App\Models\GeoLocation;
Expand All @@ -22,13 +23,17 @@
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use Lomkit\Rest\Http\Requests\SearchRequest;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void {}
public function register(): void
{
$this->app->extend(SearchRequest::class, fn ($resolved, $app) => $app->make(AppSearchRequest::class));
}

/**
* Bootstrap any application services.
Expand Down
16 changes: 16 additions & 0 deletions app/Rules/Search/Search.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Rules\Search;

use Lomkit\Rest\Rules\Search\Search as BaseSearch;

class Search extends BaseSearch
{
public function buildValidationRules(string $attribute, mixed $value): array
{
$rules = parent::buildValidationRules($attribute, $value);
$rules[$attribute.'.filters.*'] = (new SearchFilter)->setResource($this->resource);

return $rules;
}
}
60 changes: 60 additions & 0 deletions app/Rules/Search/SearchFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace App\Rules\Search;

use Illuminate\Validation\Rule;
use Lomkit\Rest\Http\Requests\RestRequest;
use Lomkit\Rest\Rules\Resource\ResourceFieldOrNested;
use Lomkit\Rest\Rules\Search\SearchFilter as BaseSearchFilter;

class SearchFilter extends BaseSearchFilter
{
public function buildValidationRules(string $attribute, mixed $value): array
{
$request = app(RestRequest::class);
$isScoutMode = $request->isScoutMode();

$fieldsValidation = $isScoutMode ?
Rule::in($this->resource->getScoutFields($request)) :
(new ResourceFieldOrNested)->setResource($this->resource);

$allowedOperators = $isScoutMode ?
['=', 'in', 'not in'] :
['=', '!=', '>', '>=', '<', '<=', 'like', 'ilike', 'not like', 'in', 'not in'];

return [
$attribute.'.field' => [
'string',
'required_without:'.$attribute.'.nested',
$fieldsValidation,
],
$attribute.'.nested' => ! $isScoutMode ? [
'sometimes',
'prohibits:'.$attribute.'.field,operator,value',
'array',
] : [
'prohibited',
],
$attribute.'.nested.*.nested' => [
'prohibited',
],
$attribute.'.nested.*' => [
(new SearchFilter)->setResource($this->resource),
],
$attribute.'.operator' => [
'string',
Rule::in($allowedOperators),
],
$attribute.'.type' => ! $isScoutMode ? [
'sometimes',
Rule::in(['or', 'and']),
] : [
'prohibited',
],
$attribute.'.value' => [
'exclude_if:'.$attribute.'.value,null',
'required_without:'.$attribute.'.nested',
],
];
}
}
72 changes: 72 additions & 0 deletions tests/Feature/Rest/MoleculeSearchIlikeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Tests\Feature\Rest;

use App\Models\Molecule;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class MoleculeSearchIlikeTest extends TestCase
{
use RefreshDatabase;

public function test_molecule_search_accepts_ilike_filter_for_case_insensitive_match(): void
{
$user = User::factory()->create();

Molecule::create([
'identifier' => 'CNP9999001.0',
'name' => 'Caffeine',
'active' => true,
]);

$response = $this->actingAs($user, 'sanctum')->postJson('/api/molecules/search', [
'search' => [
'filters' => [
['field' => 'name', 'operator' => 'ilike', 'value' => '%caffeine%'],
],
],
]);

$response->assertOk();
$response->assertJsonPath('data.0.name', 'Caffeine');
}

public function test_molecule_search_like_filter_remains_case_sensitive(): void
{
$user = User::factory()->create();

Molecule::create([
'identifier' => 'CNP9999001.0',
'name' => 'Caffeine',
'active' => true,
]);

$response = $this->actingAs($user, 'sanctum')->postJson('/api/molecules/search', [
'search' => [
'filters' => [
['field' => 'name', 'operator' => 'like', 'value' => '%caffeine%'],
],
],
]);

$response->assertOk();
$response->assertJsonPath('data', []);
}

public function test_molecule_search_rejects_unknown_filter_operator(): void
{
$user = User::factory()->create();

$response = $this->actingAs($user, 'sanctum')->postJson('/api/molecules/search', [
'search' => [
'filters' => [
['field' => 'name', 'operator' => 'invalid', 'value' => '%caffeine%'],
],
],
]);

$response->assertUnprocessable();
}
}
Loading