Skip to content

Commit b2da1da

Browse files
committed
fix: filtering by nullable fields using "in" operator
1 parent 6785649 commit b2da1da

File tree

3 files changed

+182
-6
lines changed

3 files changed

+182
-6
lines changed

src/Drivers/Standard/QueryBuilder.php

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,32 @@ function ($relationQuery) use ($relationField, $filterDescriptor) {
143143
*/
144144
protected function buildFilterQueryWhereClause(string $field, array $filterDescriptor, $query, bool $or = false)
145145
{
146+
if (is_array($filterDescriptor['value']) && in_array(null, $filterDescriptor['value'], true)) {
147+
$query = $query->{$or ? 'orWhereNull' : 'whereNull'}($field);
148+
149+
$filterDescriptor['value'] = collect($filterDescriptor['value'])->filter()->values()->toArray();
150+
151+
if (!count($filterDescriptor['value'])) {
152+
return $query;
153+
}
154+
}
155+
156+
return $this->buildFilterNestedQueryWhereClause($field, $filterDescriptor, $query, $or);
157+
}
158+
159+
/**
160+
* @param string $field
161+
* @param array $filterDescriptor
162+
* @param Builder|Relation $query
163+
* @param bool $or
164+
* @return Builder|Relation
165+
*/
166+
protected function buildFilterNestedQueryWhereClause(
167+
string $field,
168+
array $filterDescriptor,
169+
$query,
170+
bool $or = false
171+
) {
146172
if ($filterDescriptor['value'] !== null &&
147173
in_array($filterDescriptor['field'], (new $this->resourceModelClass)->getDates(), true)
148174
) {
@@ -152,9 +178,18 @@ protected function buildFilterQueryWhereClause(string $field, array $filterDescr
152178
}
153179

154180
if (!is_array($filterDescriptor['value']) || $constraint === 'whereDate') {
155-
$query->{$or ? 'or' . ucfirst($constraint) : $constraint}($field, $filterDescriptor['operator'], $filterDescriptor['value']);
181+
$query->{$or ? 'or' . ucfirst($constraint) : $constraint}(
182+
$field,
183+
$filterDescriptor['operator'],
184+
$filterDescriptor['value']
185+
);
156186
} else {
157-
$query->{$or ? 'orWhereIn' : 'whereIn'}($field, $filterDescriptor['value'], 'and', $filterDescriptor['operator'] === 'not in');
187+
$query->{$or ? 'orWhereIn' : 'whereIn'}(
188+
$field,
189+
$filterDescriptor['value'],
190+
'and',
191+
$filterDescriptor['operator'] === 'not in'
192+
);
158193
}
159194

160195
return $query;
@@ -174,11 +209,46 @@ protected function buildPivotFilterQueryWhereClause(
174209
array $filterDescriptor,
175210
$query,
176211
bool $or = false
212+
) {
213+
if (is_array($filterDescriptor['value']) && in_array(null, $filterDescriptor['value'], true)) {
214+
$query = $query->{$or ? 'orWherePivotNull' : 'wherePivotNull'}($field);
215+
216+
$filterDescriptor['value'] = collect($filterDescriptor['value'])->filter()->values()->toArray();
217+
218+
if (!count($filterDescriptor['value'])) {
219+
return $query;
220+
}
221+
}
222+
223+
return $this->buildPivotFilterNestedQueryWhereClause($field, $filterDescriptor, $query);
224+
}
225+
226+
/**
227+
* @param string $field
228+
* @param array $filterDescriptor
229+
* @param Builder|Relation $query
230+
* @param bool $or
231+
* @return Builder
232+
*/
233+
protected function buildPivotFilterNestedQueryWhereClause(
234+
string $field,
235+
array $filterDescriptor,
236+
$query,
237+
bool $or = false
177238
) {
178239
if (!is_array($filterDescriptor['value'])) {
179-
$query->{$or ? 'orWherePivot' : 'wherePivot'}($field, $filterDescriptor['operator'], $filterDescriptor['value']);
240+
$query->{$or ? 'orWherePivot' : 'wherePivot'}(
241+
$field,
242+
$filterDescriptor['operator'],
243+
$filterDescriptor['value']
244+
);
180245
} else {
181-
$query->{$or ? 'orWherePivotIn' : 'wherePivotIn'}($field, $filterDescriptor['value'], 'and', $filterDescriptor['operator'] === 'not in');
246+
$query->{$or ? 'orWherePivotIn' : 'wherePivotIn'}(
247+
$field,
248+
$filterDescriptor['value'],
249+
'and',
250+
$filterDescriptor['operator'] === 'not in'
251+
);
182252
}
183253

184254
return $query;

tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexFilteringOperationsTest.php

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,73 @@ public function getting_a_list_of_relation_resources_filtered_by_pivot_field():
2929

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

3636
$this->assertResourcesPaginated(
3737
$response,
3838
$this->makePaginator([$user->roles()->first()->toArray()], "users/{$user->id}/roles/search")
3939
);
4040
}
41+
42+
/** @test */
43+
public function getting_a_list_of_relation_resources_filtered_by_null_pivot_field_value_using_equality_operator(): void
44+
{
45+
/** @var User $user */
46+
$user = factory(User::class)->create();
47+
48+
$roleWithCustomName = factory(Role::class)->create();
49+
$roleWithoutCustomName = factory(Role::class)->create();
50+
51+
$user->roles()->attach($roleWithoutCustomName);
52+
$user->roles()->attach($roleWithCustomName, ['custom_name' => 'test-name']);
53+
54+
Gate::policy(User::class, GreenPolicy::class);
55+
Gate::policy(Role::class, GreenPolicy::class);
56+
57+
$response = $this->post("/api/users/{$user->id}/roles/search", [
58+
'filters' => [
59+
['field' => 'pivot.custom_name', 'operator' => '=', 'value' => null],
60+
],
61+
]);
62+
63+
$this->assertResourcesPaginated(
64+
$response,
65+
$this->makePaginator(
66+
[$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()],
67+
"users/{$user->id}/roles/search"
68+
)
69+
);
70+
}
71+
72+
/** @test */
73+
public function getting_a_list_of_relation_resources_filtered_by_null_pivot_field_value_using_in_operator(): void
74+
{
75+
/** @var User $user */
76+
$user = factory(User::class)->create();
77+
78+
$roleWithCustomName = factory(Role::class)->create();
79+
$roleWithoutCustomName = factory(Role::class)->create();
80+
81+
$user->roles()->attach($roleWithoutCustomName);
82+
$user->roles()->attach($roleWithCustomName, ['custom_name' => 'test-name']);
83+
84+
Gate::policy(User::class, GreenPolicy::class);
85+
Gate::policy(Role::class, GreenPolicy::class);
86+
87+
$response = $this->post("/api/users/{$user->id}/roles/search", [
88+
'filters' => [
89+
['field' => 'pivot.custom_name', 'operator' => 'in', 'value' => [null]],
90+
],
91+
]);
92+
93+
$this->assertResourcesPaginated(
94+
$response,
95+
$this->makePaginator(
96+
[$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()],
97+
"users/{$user->id}/roles/search"
98+
)
99+
);
100+
}
41101
}

tests/Feature/StandardIndexFilteringOperationsTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,52 @@ public function getting_a_list_of_resources_filtered_by_model_field_with_wildcar
371371
);
372372
}
373373

374+
/** @test */
375+
public function getting_a_list_of_resources_filtered_by_null_field_value_using_equality_operator(): void
376+
{
377+
$matchingTeam = factory(Team::class)->create(['description' => null])->fresh();
378+
factory(Team::class)->create(['description' => 'not match'])->fresh();
379+
380+
Gate::policy(Team::class, GreenPolicy::class);
381+
382+
$response = $this->post(
383+
'/api/teams/search',
384+
[
385+
'filters' => [
386+
['field' => 'description', 'operator' => '=', 'value' => null],
387+
],
388+
]
389+
);
390+
391+
$this->assertResourcesPaginated(
392+
$response,
393+
$this->makePaginator([$matchingTeam], 'teams/search')
394+
);
395+
}
396+
397+
/** @test */
398+
public function getting_a_list_of_resources_filtered_by_null_field_value_using_in_operator(): void
399+
{
400+
$matchingTeam = factory(Team::class)->create(['description' => null])->fresh();
401+
factory(Team::class)->create(['description' => 'not match'])->fresh();
402+
403+
Gate::policy(Team::class, GreenPolicy::class);
404+
405+
$response = $this->post(
406+
'/api/teams/search',
407+
[
408+
'filters' => [
409+
['field' => 'description', 'operator' => 'in', 'value' => [null]],
410+
],
411+
]
412+
);
413+
414+
$this->assertResourcesPaginated(
415+
$response,
416+
$this->makePaginator([$matchingTeam], 'teams/search')
417+
);
418+
}
419+
374420
/** @test */
375421
public function getting_a_list_of_resources_filtered_by_relation_field_with_wildcard_whitelisting(): void
376422
{

0 commit comments

Comments
 (0)