Skip to content

Commit

Permalink
fix: search on identically named relation fields
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzarbn committed Apr 3, 2023
1 parent 52ea890 commit 07b92c4
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/Contracts/RelationsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public function relationForeignKeyFromRelationInstance(Relation $relationInstanc

public function relationLocalKeyFromRelationInstance(Relation $relationInstance): string;

public function getQualifiedRelationFieldName(Relation $relation, string $field): string;

public function guardRelationsForCollection(Collection $entities, array $requestedRelations, ?string $parentRelation = null, bool $normalized = false): Collection;

public function guardRelations(Model $entity, array $requestedRelations, ?string $parentRelation = null, bool $normalized = false);
Expand Down
22 changes: 16 additions & 6 deletions src/Drivers/Standard/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,14 @@ public function applyFiltersToQuery($query, Request $request, array $filterDescr
if ($relation === 'pivot') {
$this->buildPivotFilterQueryWhereClause($relationField, $filterDescriptor, $query, $or);
} else {
$relationInstance = (new $this->resourceModelClass)->{$relation}();

$qualifiedRelationFieldName = $this->relationsResolver->getQualifiedRelationFieldName($relationInstance, $relationField);

$query->{$or ? 'orWhereHas' : 'whereHas'}(
$relation,
function ($relationQuery) use ($relationField, $filterDescriptor) {
$this->buildFilterQueryWhereClause($relationField, $filterDescriptor, $relationQuery);
function ($relationQuery) use ($qualifiedRelationFieldName, $filterDescriptor) {
$this->buildFilterQueryWhereClause($qualifiedRelationFieldName, $filterDescriptor, $relationQuery);
}
);
}
Expand Down Expand Up @@ -394,21 +398,25 @@ function ($whereQuery) use ($searchables, $requestedSearchDescriptor) {
$relation = $this->relationsResolver->relationFromParamConstraint($searchable);
$relationField = $this->relationsResolver->relationFieldFromParamConstraint($searchable);

$relationInstance = (new $this->resourceModelClass)->{$relation}();

$qualifiedRelationFieldName = $this->relationsResolver->getQualifiedRelationFieldName($relationInstance, $relationField);

$whereQuery->orWhereHas(
$relation,
function ($relationQuery) use ($relationField, $requestedSearchString, $caseSensitive) {
function ($relationQuery) use ($qualifiedRelationFieldName, $requestedSearchString, $caseSensitive) {
/**
* @var Builder $relationQuery
*/
if (!$caseSensitive) {
return $relationQuery->whereRaw(
"lower({$relationField}) like lower(?)",
"lower({$qualifiedRelationFieldName}) like lower(?)",
['%'.$requestedSearchString.'%']
);
}

return $relationQuery->where(
$relationField,
$qualifiedRelationFieldName,
'like',
'%'.$requestedSearchString.'%'
);
Expand Down Expand Up @@ -481,7 +489,9 @@ public function applySortingToQuery($query, Request $request): void
$query->leftJoin($relationTable, $relationForeignKey, '=', $relationLocalKey);
}

$query->orderBy("$relationTable.$relationField", $direction)
$qualifiedRelationFieldName = $this->relationsResolver->getQualifiedRelationFieldName($relationInstance, $relationField);

$query->orderBy($qualifiedRelationFieldName, $direction)
->select($this->getQualifiedFieldName('*'));
} else {
$query->orderBy($this->getQualifiedFieldName($sortableField), $direction);
Expand Down
18 changes: 18 additions & 0 deletions src/Drivers/Standard/RelationsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ public function relationForeignKeyFromRelationInstance(Relation $relationInstanc
) : $relationInstance->getQualifiedForeignKey();
}

/**
* Retrieve a fully-qualified field name of the given relation.
*
* @param Relation $relation
* @param string $field
* @return string
*/
public function getQualifiedRelationFieldName(Relation $relation, string $field): string
{
if ($relation instanceof MorphTo) {
return $field;
}

$table = $relation->getModel()->getTable();

return "{$table}.{$field}";
}

/**
* Resolves relation local key from the given relation instance.
*
Expand Down
57 changes: 57 additions & 0 deletions tests/Feature/StandardIndexFilteringOperationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Support\Facades\Gate;
use Orion\Tests\Fixtures\App\Models\Company;
use Orion\Tests\Fixtures\App\Models\Post;
use Orion\Tests\Fixtures\App\Models\PostMeta;
use Orion\Tests\Fixtures\App\Models\Team;
use Orion\Tests\Fixtures\App\Models\User;
use Orion\Tests\Fixtures\App\Policies\GreenPolicy;
Expand Down Expand Up @@ -645,4 +646,60 @@ public function getting_a_list_of_resources_filtered_by_nested_jsonb_array_field
$this->makePaginator([$matchingPost], 'posts/search')
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_identically_named_relation_fields(): void
{
$user = factory(User::class)->create(['name' => 'John Doe']);
$matchingPost = factory(Post::class)
->create([ 'user_id' => $user->id, ])->fresh();
factory(PostMeta::class)->create(['post_id' => $matchingPost->id, 'name' => 'test']);

factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh();

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

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'meta.name', 'operator' => '=', 'value' => 'test'],
['field' => 'user.name', 'operator' => '=', 'value' => 'John Doe'],
],
]
);

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

/** @test */
public function getting_a_list_of_resources_filtered_by_identically_named_fields_on_different_nesting_level(): void
{
$matchingPost = factory(Post::class)
->create(['title' => 'test' ])->fresh();
factory(PostMeta::class)->create(['post_id' => $matchingPost->id, 'title' => 'test']);


factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh();

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

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'meta.title', 'operator' => '=', 'value' => 'test'],
['field' => 'title', 'operator' => '=', 'value' => 'test'],
],
]
);

$this->assertResourcesPaginated(
$response,
$this->makePaginator([$matchingPost], 'posts/search')
);
}
}
4 changes: 3 additions & 1 deletion tests/Fixtures/app/Http/Controllers/PostsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public function filterableBy(): array
'position',
'publish_at',
'user.name',
'meta.name',
'meta.title',
'meta->nested_field',
'options',
'options->nested_field',
Expand All @@ -45,7 +47,7 @@ public function filterableBy(): array

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

public function exposedScopes(): array
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/app/Models/PostMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class PostMeta extends Model
*
* @var array
*/
protected $fillable = ['notes'];
protected $fillable = ['name', 'title', 'notes'];

/**
* @return BelongsTo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public function up()
{
Schema::create('post_metas', function (Blueprint $table) {
$table->increments('id');
$table->string('title')->nullable();
$table->string('name')->nullable();
$table->text('notes');
$table->boolean('comments_enabled')->default(true);
$table->unsignedBigInteger('post_id');
Expand Down

0 comments on commit 07b92c4

Please sign in to comment.