Skip to content

Commit

Permalink
Merge branch 'main' into next
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/Specs/Builders/TagsBuilder.php
  • Loading branch information
alexzarbn committed Oct 13, 2023
2 parents 7ea91b9 + 6cc031f commit 8bd8ea0
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 7 deletions.
4 changes: 4 additions & 0 deletions src/Contracts/RelationsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public function __construct(array $includableRelations, array $alwaysIncludedRel

public function requestedRelations(Request $request): array;

public function relationInstanceFromParamConstraint(string $resourceModelClass, string $paramConstraint): Relation;

public function rootRelationFromParamConstraint(string $paramConstraint): string;

public function relationFromParamConstraint(string $paramConstraint): string;

public function relationFieldFromParamConstraint(string $paramConstraint): string;
Expand Down
3 changes: 1 addition & 2 deletions src/Drivers/Standard/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ public function applyFiltersToQuery($query, Request $request, array $filterDescr
if ($relation === 'pivot') {
$this->buildPivotFilterQueryWhereClause($relationField, $filterDescriptor, $query, $or);
} else {
$relationInstance = (new $this->resourceModelClass)->{$relation}();

$relationInstance = $this->relationsResolver->relationInstanceFromParamConstraint($this->resourceModelClass, $filterDescriptor['field']);
$qualifiedRelationFieldName = $this->relationsResolver->getQualifiedRelationFieldName($relationInstance, $relationField);

$query->{$or ? 'orWhereHas' : 'whereHas'}(
Expand Down
35 changes: 34 additions & 1 deletion src/Drivers/Standard/RelationsResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -81,6 +83,37 @@ public function requestedRelations(Request $request): array
return $validatedIncludes;
}

public function relationInstanceFromParamConstraint(string $resourceModelClass, string $paramConstraint): Relation
{
$resourceModel = new $resourceModelClass();

do {
$relationName = $this->rootRelationFromParamConstraint($paramConstraint);
$paramConstraint = str_replace("{$relationName}.", '', $paramConstraint);

$relation = $resourceModel->{$relationName}();

if (in_array(get_class($relation), [MorphTo::class, MorphMany::class, MorphToMany::class, MorphOne::class])) {
break;
}

$resourceModel = $relation->getModel();
} while (str_contains($paramConstraint, '.'));

return $relation;
}

/**
* Resolves relation name from the given param constraint.
*
* @param string $paramConstraint
* @return string
*/
public function rootRelationFromParamConstraint(string $paramConstraint): string
{
return Arr::first(explode('.', $paramConstraint));
}

/**
* Resolves relation name from the given param constraint.
*
Expand Down Expand Up @@ -141,7 +174,7 @@ public function relationForeignKeyFromRelationInstance(Relation $relationInstanc
*/
public function getQualifiedRelationFieldName(Relation $relation, string $field): string
{
if ($relation instanceof MorphTo) {
if (in_array(get_class($relation), [MorphTo::class, MorphMany::class, MorphToMany::class, MorphOne::class])) {
return $field;
}

Expand Down
16 changes: 16 additions & 0 deletions src/Http/Routing/PendingResourceRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,20 @@ public function withoutBatch(): PendingResourceRegistration

return $this;
}

/**
* Disables the search operation on the resource.
*
* @return $this
*/
public function withoutSearch(): PendingResourceRegistration
{
$except = Arr::get($this->options, 'except');

$except = array_merge($except, ['search']);

$this->except($except);

return $this;
}
}
5 changes: 3 additions & 2 deletions src/Specs/Builders/TagsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ public function build(): array
$tags = collect(config('orion.specs.tags.default'));

foreach ($resources as $resource) {
$tags[] = [
'name' => $resource->tag,
if (!$tags->contains('name', $resource->tag)) $tags[] = [
'name' => $resource->tag,
'description' => "API documentation for {$resource->tag}",
];
}

Expand Down
29 changes: 29 additions & 0 deletions tests/Feature/StandardIndexFilteringOperationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,35 @@ public function getting_a_list_of_resources_filtered_by_relation_field_resources
);
}

/** @test */
public function getting_a_list_of_resources_filtered_by_deep_relation_field_resources(): void
{
$matchingPostUser = factory(User::class)->create(['name' => 'match']);
$matchingPostUser->roles()->create(['name' => 'matching-role-name']);
$matchingPost = factory(Post::class)->create(['user_id' => $matchingPostUser->id])->fresh();

$nonMatchingPostUser = factory(User::class)->create(['name' => 'not match']);
$nonMatchingPostUser->roles()->create(['name' => 'non-matching-role-name']);
factory(Post::class)->create(['user_id' => $nonMatchingPostUser->id])->fresh();

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

$response = $this->post(
'/api/posts/search',
[
'filters' => [
['field' => 'user.name', 'operator' => '=', 'value' => 'match'],
['field' => 'user.roles.name', 'operator' => '=', 'value' => 'matching-role-name'],
],
]
);

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

/** @test */
public function getting_a_list_of_resources_filtered_by_not_whitelisted_field(): void
{
Expand Down
1 change: 1 addition & 0 deletions tests/Fixtures/app/Http/Controllers/PostsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function filterableBy(): array
'position',
'publish_at',
'user.name',
'user.roles.name',
'meta.name',
'meta.title',
'meta->nested_field',
Expand Down
4 changes: 2 additions & 2 deletions tests/Fixtures/app/Traits/AppliesDefaultOrder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protected static function boot()
parent::boot();
// Order by name ASC
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('id', 'asc');
$builder->orderBy($builder->getModel()->getTable().'.id', 'asc');
});
}
}
}
45 changes: 45 additions & 0 deletions tests/Unit/OrionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -691,4 +691,49 @@ function () {
$this->assertRouteRegistered('api.projects.users.batchDestroy', ['DELETE'], 'api/projects/{project}/users/batch', DummyController::class.'@batchDestroy');
$this->assertRouteRegistered('api.projects.users.batchRestore', ['POST'], 'api/projects/{project}/users/batch/restore', DummyController::class.'@batchRestore');
}

/** @test */
public function registering_resource_without_batch_removes_routes(): void
{
Route::group(
['as' => 'api.', 'prefix' => 'api'],
function () {
Orion::resource('projects', DummyController::class)->withoutBatch();
}
);

$this->assertRouteRegistered('api.projects.search', ['POST'], 'api/projects/search', DummyController::class.'@search');
$this->assertRouteRegistered('api.projects.index', ['GET', 'HEAD'], 'api/projects', DummyController::class.'@index');
$this->assertRouteRegistered('api.projects.store', ['POST'], 'api/projects', DummyController::class.'@store');
$this->assertRouteRegistered('api.projects.show', ['GET', 'HEAD'], 'api/projects/{project}', DummyController::class.'@show');
$this->assertRouteRegistered('api.projects.update', ['PUT', 'PATCH'], 'api/projects/{project}', DummyController::class.'@update');
$this->assertRouteRegistered('api.projects.destroy', ['DELETE'], 'api/projects/{project}', DummyController::class.'@destroy');

$this->assertRouteNotRegistered('api.projects.batchStore');
$this->assertRouteNotRegistered('api.projects.batchUpdate');
$this->assertRouteNotRegistered('api.projects.batchDestroy');
}

/** @test */
public function registering_resource_without_search_removes_routes(): void
{
Route::group(
['as' => 'api.', 'prefix' => 'api'],
function () {
Orion::resource('projects', DummyController::class)->withoutSearch();
}
);

$this->assertRouteNotRegistered('api.projects.search');

$this->assertRouteRegistered('api.projects.index', ['GET', 'HEAD'], 'api/projects', DummyController::class.'@index');
$this->assertRouteRegistered('api.projects.store', ['POST'], 'api/projects', DummyController::class.'@store');
$this->assertRouteRegistered('api.projects.show', ['GET', 'HEAD'], 'api/projects/{project}', DummyController::class.'@show');
$this->assertRouteRegistered('api.projects.update', ['PUT', 'PATCH'], 'api/projects/{project}', DummyController::class.'@update');
$this->assertRouteRegistered('api.projects.destroy', ['DELETE'], 'api/projects/{project}', DummyController::class.'@destroy');

$this->assertRouteRegistered('api.projects.batchStore', ['POST'], 'api/projects/batch', DummyController::class.'@batchStore');
$this->assertRouteRegistered('api.projects.batchUpdate', ['PATCH'], 'api/projects/batch', DummyController::class.'@batchUpdate');
$this->assertRouteRegistered('api.projects.batchDestroy', ['DELETE'], 'api/projects/batch', DummyController::class.'@batchDestroy');
}
}

0 comments on commit 8bd8ea0

Please sign in to comment.