Skip to content

Commit

Permalink
feat: all in, any in filter operators
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzarbn committed Jun 20, 2022
1 parent 8e4f970 commit f063f6d
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 14 deletions.
2 changes: 1 addition & 1 deletion src/Drivers/Standard/ParamsValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function validateFilters(Request $request): void
'filters' => ['sometimes', 'array'],
'filters.*.type' => ['sometimes', 'in:and,or'],
'filters.*.field' => ['required_with:filters', 'regex:/^[\w.\_\-\>]+$/', new WhitelistedField($this->filterableBy)],
'filters.*.operator' => ['sometimes', 'in:<,<=,>,>=,=,!=,like,not like,ilike,not ilike,in,not in'],
'filters.*.operator' => ['sometimes', 'in:<,<=,>,>=,=,!=,like,not like,ilike,not ilike,in,not in,all in,any in'],
'filters.*.value' => ['present', 'nullable'],
]
)->validate();
Expand Down
37 changes: 31 additions & 6 deletions src/Drivers/Standard/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Arr;
use JsonException;
use Orion\Http\Requests\Request;
use RuntimeException;

Expand Down Expand Up @@ -62,6 +63,7 @@ public function __construct(
* @param Builder|Relation $query
* @param Request $request
* @return Builder|Relation
* @throws JsonException
*/
public function buildQuery($query, Request $request)
{
Expand Down Expand Up @@ -102,6 +104,7 @@ public function applyScopesToQuery($query, Request $request): void
* @param Builder|Relation $query
* @param Request $request
* @param array $filterDescriptors
* @throws JsonException
*/
public function applyFiltersToQuery($query, Request $request, array $filterDescriptors = []): void
{
Expand Down Expand Up @@ -146,6 +149,7 @@ function ($relationQuery) use ($relationField, $filterDescriptor) {
* @param Builder|Relation $query
* @param bool $or
* @return Builder|Relation
* @throws JsonException
*/
protected function buildFilterQueryWhereClause(string $field, array $filterDescriptor, $query, bool $or = false)
{
Expand All @@ -168,6 +172,7 @@ protected function buildFilterQueryWhereClause(string $field, array $filterDescr
* @param Builder|Relation $query
* @param bool $or
* @return Builder|Relation
* @throws JsonException
*/
protected function buildFilterNestedQueryWhereClause(
string $field,
Expand All @@ -180,16 +185,36 @@ protected function buildFilterNestedQueryWhereClause(

if ($treatAsDateField && Carbon::parse($filterDescriptor['value'])->toTimeString() === '00:00:00') {
$constraint = 'whereDate';
} elseif (in_array(Arr::get($filterDescriptor, 'operator'), ['all in', 'any in'])) {
$constraint = 'whereJsonContains';
} else {
$constraint = 'where';
}

if (!is_array($filterDescriptor['value']) || $constraint === 'whereDate') {
$query->{$or ? 'or' . ucfirst($constraint) : $constraint}(
if ($constraint !== 'whereJsonContains' && (!is_array(
$filterDescriptor['value']
) || $constraint === 'whereDate')) {
$query->{$or ? 'or'.ucfirst($constraint) : $constraint}(
$field,
$filterDescriptor['operator'] ?? '=',
$filterDescriptor['value']
);
} elseif ($constraint === 'whereJsonContains') {
if (!is_array($filterDescriptor['value'])) {
$query->{$or ? 'orWhereJsonContains' : 'whereJsonContains'}(
$field,
$filterDescriptor['value']
);
} else {
$query->{$or ? 'orWhere' : 'where'}(function ($nestedQuery) use ($filterDescriptor, $field) {
foreach ($filterDescriptor['value'] as $value) {
$nestedQuery->{$filterDescriptor['operator'] === 'any in' ? 'orWhereJsonContains' : 'whereJsonContains'}(
$field,
$value
);
}
});
}
} else {
$query->{$or ? 'orWhereIn' : 'whereIn'}(
$field,
Expand Down Expand Up @@ -335,14 +360,14 @@ function ($relationQuery) use ($relationField, $requestedSearchString, $caseSens
if (!$caseSensitive) {
return $relationQuery->whereRaw(
"lower({$relationField}) like lower(?)",
['%' . $requestedSearchString . '%']
['%'.$requestedSearchString.'%']
);
}

return $relationQuery->where(
$relationField,
'like',
'%' . $requestedSearchString . '%'
'%'.$requestedSearchString.'%'
);
}
);
Expand All @@ -352,13 +377,13 @@ function ($relationQuery) use ($relationField, $requestedSearchString, $caseSens
if (!$caseSensitive) {
$whereQuery->orWhereRaw(
"lower({$qualifiedFieldName}) like lower(?)",
['%' . $requestedSearchString . '%']
['%'.$requestedSearchString.'%']
);
} else {
$whereQuery->orWhere(
$qualifiedFieldName,
'like',
'%' . $requestedSearchString . '%'
'%'.$requestedSearchString.'%'
);
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Specs/Builders/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Orion\Specs\Builders;

use Illuminate\Contracts\Container\BindingResolutionException;

class Builder
{
/** @var InfoBuilder */
Expand Down Expand Up @@ -35,6 +37,10 @@ public function __construct(
$this->tagsBuilder = $tagsBuilder;
}

/**
* @return array
* @throws BindingResolutionException
*/
public function build(): array
{
return [
Expand Down
112 changes: 112 additions & 0 deletions tests/Feature/StandardIndexFilteringOperationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -533,4 +533,116 @@ public function getting_a_list_of_resources_filtered_by_model_datetime_field():
$this->makePaginator([$matchingPost], 'posts/search')
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_jsonb_array_field_inclusive(): void
{
if (config('database.default') === 'sqlite'){
$this->markTestSkipped('Not supported with SQLite');
}

$matchingPost = factory(Post::class)
->create(['options' => ['a', 'b', 'c']])->fresh();
factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh();

Gate::policy(Post::class, GreenPolicy::class);

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'options', 'operator' => 'all in', 'value' => ['a', 'b']],
],
]
);

$this->assertResourcesPaginated(
$response,
$this->makePaginator([$matchingPost], 'posts/search')
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_nested_jsonb_array_field_inclusive(): void
{
if (config('database.default') === 'sqlite'){
$this->markTestSkipped('Not supported with SQLite');
}

$matchingPost = factory(Post::class)
->create(['options' => ['nested_field' => ['a', 'b', 'c']]])->fresh();
factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh();

Gate::policy(Post::class, GreenPolicy::class);

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'options->nested_field', 'operator' => 'all in', 'value' => ['a', 'b']],
],
]
);

$this->assertResourcesPaginated(
$response,
$this->makePaginator([$matchingPost], 'posts/search')
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_jsonb_array_field_exclusive(): void
{
if (config('database.default') === 'sqlite'){
$this->markTestSkipped('Not supported with SQLite');
}

$matchingPost = factory(Post::class)
->create(['options' => ['a', 'd']])->fresh();
factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh();

Gate::policy(Post::class, GreenPolicy::class);

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'options', 'operator' => 'any in', 'value' => ['a', 'b']],
],
]
);

$this->assertResourcesPaginated(
$response,
$this->makePaginator([$matchingPost], 'posts/search')
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_nested_jsonb_array_field_exclusive(): void
{
if (config('database.default') === 'sqlite'){
$this->markTestSkipped('Not supported with SQLite');
}

$matchingPost = factory(Post::class)
->create(['options' => ['nested_field' => ['a', 'd']]])->fresh();
factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh();

Gate::policy(Post::class, GreenPolicy::class);

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'options->nested_field', 'operator' => 'any in', 'value' => ['a', 'b']],
],
]
);

$this->assertResourcesPaginated(
$response,
$this->makePaginator([$matchingPost], 'posts/search')
);
}
}
20 changes: 14 additions & 6 deletions tests/Fixtures/app/Http/Controllers/PostsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,38 @@ protected function beforeSave(Request $request, $entity)
}
}

public function sortableBy() : array
public function sortableBy(): array
{
return ['title', 'user.name', 'meta->nested_field'];
}

public function filterableBy() : array
public function filterableBy(): array
{
return ['title', 'position', 'publish_at', 'user.name', 'meta->nested_field'];
return [
'title',
'position',
'publish_at',
'user.name',
'meta->nested_field',
'options',
'options->nested_field',
];
}

public function searchableBy() : array
public function searchableBy(): array
{
return ['title', 'user.name'];
}

public function exposedScopes() : array
public function exposedScopes(): array
{
return ['published', 'publishedAt'];
}

/**
* @return array
*/
public function includes() : array
public function includes(): array
{
return ['user'];
}
Expand Down
3 changes: 2 additions & 1 deletion tests/Fixtures/app/Models/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class Post extends Model
* @var array
*/
protected $casts = [
'meta' => 'array'
'meta' => 'array',
'options' => 'array',
];

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public function up()
$table->text('body');
$table->string('tracking_id')->nullable();
$table->jsonb('meta')->nullable();
$table->jsonb('options')->nullable();
$table->unsignedInteger('position')->default(0);

$table->unsignedBigInteger('user_id')->nullable();
Expand Down

0 comments on commit f063f6d

Please sign in to comment.