Skip to content

Commit

Permalink
fix: filtering by nullable fields using "in" operator
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzarbn committed Dec 8, 2021
1 parent 6785649 commit b2da1da
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 6 deletions.
78 changes: 74 additions & 4 deletions src/Drivers/Standard/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ function ($relationQuery) use ($relationField, $filterDescriptor) {
*/
protected function buildFilterQueryWhereClause(string $field, array $filterDescriptor, $query, bool $or = false)
{
if (is_array($filterDescriptor['value']) && in_array(null, $filterDescriptor['value'], true)) {
$query = $query->{$or ? 'orWhereNull' : 'whereNull'}($field);

$filterDescriptor['value'] = collect($filterDescriptor['value'])->filter()->values()->toArray();

if (!count($filterDescriptor['value'])) {
return $query;
}
}

return $this->buildFilterNestedQueryWhereClause($field, $filterDescriptor, $query, $or);
}

/**
* @param string $field
* @param array $filterDescriptor
* @param Builder|Relation $query
* @param bool $or
* @return Builder|Relation
*/
protected function buildFilterNestedQueryWhereClause(
string $field,
array $filterDescriptor,
$query,
bool $or = false
) {
if ($filterDescriptor['value'] !== null &&
in_array($filterDescriptor['field'], (new $this->resourceModelClass)->getDates(), true)
) {
Expand All @@ -152,9 +178,18 @@ protected function buildFilterQueryWhereClause(string $field, array $filterDescr
}

if (!is_array($filterDescriptor['value']) || $constraint === 'whereDate') {
$query->{$or ? 'or' . ucfirst($constraint) : $constraint}($field, $filterDescriptor['operator'], $filterDescriptor['value']);
$query->{$or ? 'or' . ucfirst($constraint) : $constraint}(
$field,
$filterDescriptor['operator'],
$filterDescriptor['value']
);
} else {
$query->{$or ? 'orWhereIn' : 'whereIn'}($field, $filterDescriptor['value'], 'and', $filterDescriptor['operator'] === 'not in');
$query->{$or ? 'orWhereIn' : 'whereIn'}(
$field,
$filterDescriptor['value'],
'and',
$filterDescriptor['operator'] === 'not in'
);
}

return $query;
Expand All @@ -174,11 +209,46 @@ protected function buildPivotFilterQueryWhereClause(
array $filterDescriptor,
$query,
bool $or = false
) {
if (is_array($filterDescriptor['value']) && in_array(null, $filterDescriptor['value'], true)) {
$query = $query->{$or ? 'orWherePivotNull' : 'wherePivotNull'}($field);

$filterDescriptor['value'] = collect($filterDescriptor['value'])->filter()->values()->toArray();

if (!count($filterDescriptor['value'])) {
return $query;
}
}

return $this->buildPivotFilterNestedQueryWhereClause($field, $filterDescriptor, $query);
}

/**
* @param string $field
* @param array $filterDescriptor
* @param Builder|Relation $query
* @param bool $or
* @return Builder
*/
protected function buildPivotFilterNestedQueryWhereClause(
string $field,
array $filterDescriptor,
$query,
bool $or = false
) {
if (!is_array($filterDescriptor['value'])) {
$query->{$or ? 'orWherePivot' : 'wherePivot'}($field, $filterDescriptor['operator'], $filterDescriptor['value']);
$query->{$or ? 'orWherePivot' : 'wherePivot'}(
$field,
$filterDescriptor['operator'],
$filterDescriptor['value']
);
} else {
$query->{$or ? 'orWherePivotIn' : 'wherePivotIn'}($field, $filterDescriptor['value'], 'and', $filterDescriptor['operator'] === 'not in');
$query->{$or ? 'orWherePivotIn' : 'wherePivotIn'}(
$field,
$filterDescriptor['value'],
'and',
$filterDescriptor['operator'] === 'not in'
);
}

return $query;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,73 @@ public function getting_a_list_of_relation_resources_filtered_by_pivot_field():

$response = $this->post("/api/users/{$user->id}/roles/search", [
'filters' => [
['field' => 'pivot.custom_name', 'operator' => '=', 'value' => 'test-name']
]
['field' => 'pivot.custom_name', 'operator' => '=', 'value' => 'test-name'],
],
]);

$this->assertResourcesPaginated(
$response,
$this->makePaginator([$user->roles()->first()->toArray()], "users/{$user->id}/roles/search")
);
}

/** @test */
public function getting_a_list_of_relation_resources_filtered_by_null_pivot_field_value_using_equality_operator(): void
{
/** @var User $user */
$user = factory(User::class)->create();

$roleWithCustomName = factory(Role::class)->create();
$roleWithoutCustomName = factory(Role::class)->create();

$user->roles()->attach($roleWithoutCustomName);
$user->roles()->attach($roleWithCustomName, ['custom_name' => 'test-name']);

Gate::policy(User::class, GreenPolicy::class);
Gate::policy(Role::class, GreenPolicy::class);

$response = $this->post("/api/users/{$user->id}/roles/search", [
'filters' => [
['field' => 'pivot.custom_name', 'operator' => '=', 'value' => null],
],
]);

$this->assertResourcesPaginated(
$response,
$this->makePaginator(
[$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()],
"users/{$user->id}/roles/search"
)
);
}

/** @test */
public function getting_a_list_of_relation_resources_filtered_by_null_pivot_field_value_using_in_operator(): void
{
/** @var User $user */
$user = factory(User::class)->create();

$roleWithCustomName = factory(Role::class)->create();
$roleWithoutCustomName = factory(Role::class)->create();

$user->roles()->attach($roleWithoutCustomName);
$user->roles()->attach($roleWithCustomName, ['custom_name' => 'test-name']);

Gate::policy(User::class, GreenPolicy::class);
Gate::policy(Role::class, GreenPolicy::class);

$response = $this->post("/api/users/{$user->id}/roles/search", [
'filters' => [
['field' => 'pivot.custom_name', 'operator' => 'in', 'value' => [null]],
],
]);

$this->assertResourcesPaginated(
$response,
$this->makePaginator(
[$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()],
"users/{$user->id}/roles/search"
)
);
}
}
46 changes: 46 additions & 0 deletions tests/Feature/StandardIndexFilteringOperationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,52 @@ public function getting_a_list_of_resources_filtered_by_model_field_with_wildcar
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_null_field_value_using_equality_operator(): void
{
$matchingTeam = factory(Team::class)->create(['description' => null])->fresh();
factory(Team::class)->create(['description' => 'not match'])->fresh();

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

$response = $this->post(
'/api/teams/search',
[
'filters' => [
['field' => 'description', 'operator' => '=', 'value' => null],
],
]
);

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

/** @test */
public function getting_a_list_of_resources_filtered_by_null_field_value_using_in_operator(): void
{
$matchingTeam = factory(Team::class)->create(['description' => null])->fresh();
factory(Team::class)->create(['description' => 'not match'])->fresh();

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

$response = $this->post(
'/api/teams/search',
[
'filters' => [
['field' => 'description', 'operator' => 'in', 'value' => [null]],
],
]
);

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

/** @test */
public function getting_a_list_of_resources_filtered_by_relation_field_with_wildcard_whitelisting(): void
{
Expand Down

0 comments on commit b2da1da

Please sign in to comment.