diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b0a7db785..f0196afa5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: fail-fast: true matrix: php: [7.4] - laravel: [^6.0, ^7.0] + laravel: [^7.0] name: P${{ matrix.php }} - L${{ matrix.laravel }} diff --git a/composer.json b/composer.json index 9134d597d..e228d59c4 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,9 @@ "php": "^7.4", "ext-json": "*", "doctrine/dbal": "^2.10", - "illuminate/support": "^6.0|^7.0", - "laravel/ui": "^2.0" + "illuminate/support": "^7.0", + "laravel/ui": "^2.0", + "spatie/once": "^2.2" }, "require-dev": { "mockery/mockery": "^1.3", diff --git a/src/Http/Requests/InteractWithRepositories.php b/src/Http/Requests/InteractWithRepositories.php index 8513ae7b5..991f2cee9 100644 --- a/src/Http/Requests/InteractWithRepositories.php +++ b/src/Http/Requests/InteractWithRepositories.php @@ -89,23 +89,6 @@ public function newRepository() return $repository::resolveWith($repository::newModel()); } - /** - * Check if the route is resolved by the Repository class, or it uses the classical Models. - * @return bool - */ - public function isResolvedByRestify() - { - try { - $this->repository(); - - return true; - } catch (EntityNotFoundException $e) { - return false; - } catch (UnauthorizedException $e) { - return true; - } - } - /** * Get a new instance of the repository being requested. * As a model it could accept either a model instance, a collection or even paginated collection. @@ -131,7 +114,11 @@ public function newRepositoryWith($model, $uriKey = null) */ public function newQueryWithoutScopes($uriKey = null) { - return $this->model($uriKey)->newQueryWithoutScopes(); + if (! $this->isViaRepository()) { + return $this->model($uriKey)->newQueryWithoutScopes(); + } + + return $this->viaQuery(); } /** @@ -164,4 +151,16 @@ public function findModelQuery($repositoryId = null, $uriKey = null) $repositoryId ?? request('repositoryId') ); } + + public function viaParentModel() + { + $parent = $this->repository($this->viaRepository); + + return once(fn () => $parent::newModel()->newQueryWithoutScopes()->whereKey($this->viaRepositoryId)->firstOrFail()); + } + + public function viaQuery() + { + return $this->viaParentModel()->{$this->viaRelationship}(); + } } diff --git a/src/Http/Requests/RestifyRequest.php b/src/Http/Requests/RestifyRequest.php index 5cc6cff1d..86d22e25b 100644 --- a/src/Http/Requests/RestifyRequest.php +++ b/src/Http/Requests/RestifyRequest.php @@ -57,4 +57,9 @@ public function isStoreRequest() { return $this instanceof RepositoryStoreRequest; } + + public function isViaRepository() + { + return $this->viaRepository && $this->viaRepositoryId && $this->viaRelationship; + } } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index e0ede5084..b4b4f1c82 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -13,7 +13,6 @@ use Binaryk\LaravelRestify\Traits\InteractWithSearch; use Binaryk\LaravelRestify\Traits\PerformsQueries; use Illuminate\Contracts\Pagination\LengthAwarePaginator; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; @@ -179,9 +178,13 @@ public static function newModel(): Model return new $model; } - public static function query(): Builder + public static function query(RestifyRequest $request) { - return static::newModel()->query(); + if (! $request->isViaRepository()) { + return static::newModel()->query(); + } + + return $request->viaQuery(); } /** @@ -493,7 +496,11 @@ public function index(RestifyRequest $request) * @var AbstractPaginator $paginator */ $paginator = RepositorySearchService::instance()->search($request, $this) - ->paginate($request->perPage ?? (static::$defaultPerPage ?? RestifySearchable::DEFAULT_PER_PAGE)); + ->paginate( + $request->isViaRepository() + ? static::$defaultRelatablePerPage + : ($request->perPage ?? static::$defaultPerPage) + ); $items = $paginator->getCollection()->map(function ($value) { return static::resolveWith($value); @@ -520,7 +527,12 @@ public function store(RestifyRequest $request) $request, $this->resource, $this->storeFields($request) ); - $this->resource->save(); + if ($request->isViaRepository()) { + $this->resource = $request->viaQuery() + ->save($this->resource); + } else { + $this->resource->save(); + } $this->storeFields($request)->each(fn (Field $field) => $field->invokeAfter($request, $this->resource)); }); diff --git a/src/Services/Search/RepositorySearchService.php b/src/Services/Search/RepositorySearchService.php index 1cbf6dbf1..cdb304d96 100644 --- a/src/Services/Search/RepositorySearchService.php +++ b/src/Services/Search/RepositorySearchService.php @@ -5,7 +5,6 @@ use Binaryk\LaravelRestify\Contracts\RestifySearchable; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Repositories\Repository; -use Illuminate\Database\Eloquent\Builder; class RepositorySearchService extends Searchable { @@ -15,7 +14,7 @@ public function search(RestifyRequest $request, Repository $repository) { $this->repository = $repository; - $query = $this->prepareMatchFields($request, $this->prepareSearchFields($request, $repository::query(), $this->fixedInput), $this->fixedInput); + $query = $this->prepareMatchFields($request, $this->prepareSearchFields($request, $repository::query($request), $this->fixedInput), $this->fixedInput); return tap($this->prepareRelations($request, $this->prepareOrders($request, $query), $this->fixedInput), $this->applyIndexQuery($request, $repository)); } @@ -66,7 +65,7 @@ public function prepareMatchFields(RestifyRequest $request, $query, $extra = []) return $query; } - public function prepareOrders(RestifyRequest $request, $query, $extra = []): Builder + public function prepareOrders(RestifyRequest $request, $query, $extra = []) { $sort = $request->get('sort', ''); @@ -89,7 +88,7 @@ public function prepareOrders(RestifyRequest $request, $query, $extra = []): Bui return $query; } - public function prepareRelations(RestifyRequest $request, $query, $extra = []): Builder + public function prepareRelations(RestifyRequest $request, $query, $extra = []) { $relations = array_merge($extra, explode(',', $request->get('with'))); @@ -102,12 +101,12 @@ public function prepareRelations(RestifyRequest $request, $query, $extra = []): return $query; } - public function prepareSearchFields(RestifyRequest $request, $query, $extra = []): Builder + public function prepareSearchFields(RestifyRequest $request, $query, $extra = []) { $search = $request->get('search', data_get($extra, 'search', '')); $model = $query->getModel(); - $query->where(function (Builder $query) use ($search, $model) { + $query->where(function ($query) use ($search, $model) { $connectionType = $model->getConnection()->getDriverName(); $canSearchPrimaryKey = is_numeric($search) && diff --git a/tests/Controllers/RelatedIndexControllerTest.php b/tests/Controllers/RelatedIndexControllerTest.php new file mode 100644 index 000000000..41cffc2d0 --- /dev/null +++ b/tests/Controllers/RelatedIndexControllerTest.php @@ -0,0 +1,125 @@ +mockUsers(); + $this->mockPosts(1, 10); + + $this->mockPosts( + factory(User::class)->create()->id + ); + + $response = $this->getJson('restify-api/posts?viaRepository=users&viaRepositoryId=1&viaRelationship=posts') + ->assertStatus(200); + + $this->assertCount(10, $response->json('data')); + } + + public function test_can_show_post_belongs_to_a_user() + { + factory(User::class)->create(); + factory(User::class)->create(); + + factory(Post::class)->create([ + 'user_id' => 2, + 'title' => 'First Post', + ]); + + factory(Post::class)->create([ + 'user_id' => 1, + 'title' => 'Second Post', + ]); + + $this->getJson('restify-api/posts/1?viaRepository=users&viaRepositoryId=1&viaRelationship=posts') + ->assertStatus(404); + + $count = $this->getJson('restify-api/posts/2?viaRepository=users&viaRepositoryId=1&viaRelationship=posts') + ->assertStatus(200); + + $this->assertCount(1, $count->json()); + } + + public function test_can_store_post_belongs_to_a_user() + { + factory(User::class)->create(); + + factory(User::class)->create(); + + $this->postJson('restify-api/posts?viaRepository=users&viaRepositoryId=1&viaRelationship=posts', [ + 'title' => 'Created for the user 1', + ]) + ->assertStatus(201); + + $belongsFirst = $this->getJson('restify-api/posts?viaRepository=users&viaRepositoryId=1&viaRelationship=posts') + ->assertStatus(200); + + $belongsSecond = $this->getJson('restify-api/posts?viaRepository=users&viaRepositoryId=2&viaRelationship=posts') + ->assertStatus(200); + + $this->assertCount(1, $belongsFirst->json('data')); + $this->assertCount(0, $belongsSecond->json('data')); + } + + public function test_can_update_post_belongs_to_a_user() + { + factory(User::class)->create(); + factory(User::class)->create(); + + factory(Post::class)->create(['title' => 'Post title', 'user_id' => 1]); + + factory(Post::class)->create(['title' => 'Post title', 'user_id' => 2]); + + $response = $this->putJson('restify-api/posts/1?viaRepository=users&viaRepositoryId=1&viaRelationship=posts', [ + 'title' => 'Post updated title', + ])->assertStatus(200); + + $this->putJson('restify-api/posts/2?viaRepository=users&viaRepositoryId=1&viaRelationship=posts', [ + 'title' => 'Post updated title', + ])->assertStatus(404); + + $this->assertEquals('Post updated title', $response->json('data.attributes.title')); + } + + public function test_can_destroy_post_belongs_to_a_user() + { + factory(User::class)->create(); + factory(User::class)->create(); + + factory(Post::class)->create(['title' => 'Post title', 'user_id' => 1]); + + factory(Post::class)->create(['title' => 'Post title', 'user_id' => 2]); + + $this->deleteJson('restify-api/posts/1?viaRepository=users&viaRepositoryId=1&viaRelationship=posts')->assertStatus(204); + + $this->deleteJson('restify-api/posts/2?viaRepository=users&viaRepositoryId=1&viaRelationship=posts')->assertStatus(404); + } + + public function test_policy_check_before_destroy_post_belongs_to_a_user() + { + $_SERVER['restify.post.deletable'] = false; + + Gate::policy(Post::class, PostPolicy::class); + + factory(User::class)->create(); + + factory(User::class)->create(); + + factory(Post::class)->create(['title' => 'Post title', 'user_id' => 1]); + + factory(Post::class)->create(['title' => 'Post title', 'user_id' => 2]); + + $this->deleteJson('restify-api/posts/1?viaRepository=users&viaRepositoryId=1&viaRelationship=posts')->assertStatus(403); + + $this->deleteJson('restify-api/posts/2?viaRepository=users&viaRepositoryId=1&viaRelationship=posts')->assertStatus(404); + } +}