From 076ad3cd3215672ba132756c52feef0c9d7ebc4a Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Wed, 4 May 2022 16:32:53 +0300 Subject: [PATCH 1/2] feat: bulk deletion --- routes/api.php | 1 + src/Commands/stubs/policy.stub | 5 +++ .../RepositoryDestroyBulkController.php | 35 +++++++++++++++++ .../Requests/RepositoryDestroyBulkRequest.php | 7 ++++ src/Repositories/Repository.php | 24 ++++++++++++ src/Traits/AuthorizableModels.php | 5 +++ src/helpers.php | 2 +- .../RepositoryDestroyBulkControllerTest.php | 39 +++++++++++++++++++ tests/Fixtures/Post/PostPolicy.php | 5 +++ 9 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/Http/Controllers/RepositoryDestroyBulkController.php create mode 100644 src/Http/Requests/RepositoryDestroyBulkRequest.php create mode 100644 tests/Controllers/RepositoryDestroyBulkControllerTest.php diff --git a/routes/api.php b/routes/api.php index 80d6c29f9..7c37665b0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -36,6 +36,7 @@ Route::post('/{repository}', \Binaryk\LaravelRestify\Http\Controllers\RepositoryStoreController::class)->name('restify.store'); Route::post('/{repository}/bulk', \Binaryk\LaravelRestify\Http\Controllers\RepositoryStoreBulkController::class)->name('restify.store.bulk'); Route::post('/{repository}/bulk/update', \Binaryk\LaravelRestify\Http\Controllers\RepositoryUpdateBulkController::class)->name('restify.update.bulk'); +Route::delete('/{repository}/bulk/delete', \Binaryk\LaravelRestify\Http\Controllers\RepositoryDestroyBulkController::class)->name('restify.destroy.bulk'); Route::get('/{repository}/{repositoryId}', \Binaryk\LaravelRestify\Http\Controllers\RepositoryShowController::class)->name('restify.show'); Route::patch('/{repository}/{repositoryId}', \Binaryk\LaravelRestify\Http\Controllers\RepositoryPatchController::class)->name('restify.patch'); Route::put('/{repository}/{repositoryId}', \Binaryk\LaravelRestify\Http\Controllers\RepositoryUpdateController::class)->name('restify.put'); diff --git a/src/Commands/stubs/policy.stub b/src/Commands/stubs/policy.stub index f24f10454..98788d541 100644 --- a/src/Commands/stubs/policy.stub +++ b/src/Commands/stubs/policy.stub @@ -40,6 +40,11 @@ class {{ class }} // } + public function deleteBulk(User $user, {{ model }} $model) + { + // + } + public function delete(User $user, {{ model }} $model) { // diff --git a/src/Http/Controllers/RepositoryDestroyBulkController.php b/src/Http/Controllers/RepositoryDestroyBulkController.php new file mode 100644 index 000000000..e45f60801 --- /dev/null +++ b/src/Http/Controllers/RepositoryDestroyBulkController.php @@ -0,0 +1,35 @@ +collect() + ->each(function (int|string $key, int $row) use ($request) { + $model = $request->modelQuery($key)->lockForUpdate()->firstOrFail(); + + /** + * @var Repository $repository + */ + $repository = $request->repositoryWith($model); + + return $repository + ->allowToDestroyBulk($request) + ->deleteBulk( + $request, + $key, + $row + ); + }); + }); + + return ok(); + } +} diff --git a/src/Http/Requests/RepositoryDestroyBulkRequest.php b/src/Http/Requests/RepositoryDestroyBulkRequest.php new file mode 100644 index 000000000..58950dfea --- /dev/null +++ b/src/Http/Requests/RepositoryDestroyBulkRequest.php @@ -0,0 +1,7 @@ +json(); } + public function deleteBulk(RestifyRequest $request, $repositoryId, int $row) + { + $status = DB::transaction(function () use ($request) { + if (in_array(HasActionLogs::class, class_uses_recursive($this->resource))) { + Restify::actionLog() + ->forRepositoryDestroy($this->resource, $request->user()) + ->save(); + } + + return $this->resource->delete(); + }); + + static::deleted($status, $request); + + return ok(code: 204); + } + public function attach(RestifyRequest $request, $repositoryId, Collection $pivots) { $eagerField = $this->authorizeBelongsToMany($request)->belongsToManyField($request); @@ -919,6 +936,13 @@ public function allowToUpdateBulk(RestifyRequest $request, $payload = null): sel return $this; } + public function allowToDestroyBulk(RestifyRequest $request, $payload = null): self + { + $this->authorizeToDeleteBulk($request); + + return $this; + } + public function allowToStore(RestifyRequest $request, $payload = null): self { static::authorizeToStore($request); diff --git a/src/Traits/AuthorizableModels.php b/src/Traits/AuthorizableModels.php index fd25b5ce2..9fb99a28c 100644 --- a/src/Traits/AuthorizableModels.php +++ b/src/Traits/AuthorizableModels.php @@ -181,6 +181,11 @@ public function authorizeToUpdateBulk(Request $request) $this->authorizeTo($request, 'updateBulk'); } + public function authorizeToDeleteBulk(Request $request) + { + $this->authorizeTo($request, 'deleteBulk'); + } + /** * Determine if the current user can update the given resource. * diff --git a/src/helpers.php b/src/helpers.php index 9122940ac..7de1a1f46 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -36,7 +36,7 @@ function ok(string $message = null, int $code = 200) ], $code); } - return response()->json([], 204); + return response()->json([], $code); } } diff --git a/tests/Controllers/RepositoryDestroyBulkControllerTest.php b/tests/Controllers/RepositoryDestroyBulkControllerTest.php new file mode 100644 index 000000000..019d8aee6 --- /dev/null +++ b/tests/Controllers/RepositoryDestroyBulkControllerTest.php @@ -0,0 +1,39 @@ +authenticate(); + } + + public function test_basic_bulk_delete_works(): void + { + Gate::policy(Post::class, PostPolicy::class); + + $post1 = Post::factory()->create(); + $post2 = Post::factory()->create(); + $post3 = Post::factory()->create(); + + $this->withoutExceptionHandling(); + + $this->deleteJson(PostRepository::to('bulk/delete'), [ + $post1->getKey(), + $post2->getKey(), + ])->assertOk(); + + $this->assertModelMissing($post1); + $this->assertModelMissing($post2); + $this->assertModelExists($post3); + } +} diff --git a/tests/Fixtures/Post/PostPolicy.php b/tests/Fixtures/Post/PostPolicy.php index 39ab0d187..817c6ad18 100644 --- a/tests/Fixtures/Post/PostPolicy.php +++ b/tests/Fixtures/Post/PostPolicy.php @@ -46,6 +46,11 @@ public function update($user, $post) return $_SERVER['restify.post.update'] ?? true; } + public function deleteBulk($user, $post) + { + return $_SERVER['restify.post.deleteBulk'] ?? true; + } + public function delete($user, $post) { return $_SERVER['restify.post.delete'] ?? true; From 239b1daec23b4a9214ae2e92bb08f91a5dba9185 Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Wed, 4 May 2022 16:38:18 +0300 Subject: [PATCH 2/2] fix: docs --- docs-v2/content/en/api/repositories.md | 43 +++++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/docs-v2/content/en/api/repositories.md b/docs-v2/content/en/api/repositories.md index 938c04d6d..af3967e26 100644 --- a/docs-v2/content/en/api/repositories.md +++ b/docs-v2/content/en/api/repositories.md @@ -56,21 +56,22 @@ for example: `UserPostRepository` class has the model `UserPost`. Having this in place you're basically ready for the CRUD actions over posts. You have available the follow endpoints: -| Verb | URI | Action | -| :------------- |:----------------------------- | :-------| -| **GET** | `/api/restify/posts` | index | -| **GET** | `/api/restify/posts/actions` | index actions | -| **GET** | `/api/restify/posts/{post}` | show | -| **GET** | `/api/restify/posts/{post}/actions` | individual actions | -| **POST** | `/api/restify/posts` | store | -| **POST** | `/api/restify/posts/actions?action=actionName` | perform index actions | -| **POST** | `/api/restify/posts/bulk` | store multiple | -| **POST** | `/api/restify/posts/bulk/update` | update multiple | -| **PATCH** | `/api/restify/posts/{post}` | partial update | -| **PUT** | `/api/restify/posts/{post}` | full update | -| **POST** | `/api/restify/posts/{post}` | partial of full update including attachments | -| **POST** | `/api/restify/posts/{post}/actions?action=actionName` | perform index actions | -| **DELETE** | `/api/restify/posts/{post}` | destroy | +| Verb | URI | Action | +|:-----------|:------------------------------------------------------|:---------------------------------------------| +| **GET** | `/api/restify/posts` | index | +| **GET** | `/api/restify/posts/actions` | index actions | +| **GET** | `/api/restify/posts/{post}` | show | +| **GET** | `/api/restify/posts/{post}/actions` | individual actions | +| **POST** | `/api/restify/posts` | store | +| **POST** | `/api/restify/posts/actions?action=actionName` | perform index actions | +| **POST** | `/api/restify/posts/bulk` | store multiple | +| **DELETE** | `/api/restify/posts/bulk/delete` | delete multiple | +| **POST** | `/api/restify/posts/bulk/update` | update multiple | +| **PATCH** | `/api/restify/posts/{post}` | partial update | +| **PUT** | `/api/restify/posts/{post}` | full update | +| **POST** | `/api/restify/posts/{post}` | partial of full update including attachments | +| **POST** | `/api/restify/posts/{post}/actions?action=actionName` | perform index actions | +| **DELETE** | `/api/restify/posts/{post}` | destroy | @@ -662,6 +663,18 @@ Payload: ] ``` +### Bulk delete flow + +The payload for a bulk delete should contain an array of primary keys for the models you want to delete: + +```json +[ + 1, 10, 15 +] +``` + +These models will be resolved from the database and check for the `deleteBulk` policy permission, in case any of the models isn't allowed to be deleted, no entry will be deleted. + ## Force eager loading However, Laravel Restify [provides eager](/search/) loading based on the query `related` property, you may want to force