From 62c2fdb9c60177830a831216dcceee3d40a98d62 Mon Sep 17 00:00:00 2001 From: Fery Wardiyanto Date: Sun, 31 Mar 2024 08:17:27 +0700 Subject: [PATCH] chore: make restoration of soft-deleted record works with policy Signed-off-by: Fery Wardiyanto --- routes/base.php | 18 ++++----- src/Http/Controllers/CompanyController.php | 30 +++++++------- src/Http/Controllers/PersonnelController.php | 24 ++++++------ src/Http/Resources/AsAddress.php | 4 -- src/Repository.php | 41 +++++++++++++------- src/ServiceProvider.php | 16 ++------ tests/Feature/Http/AddressTest.php | 23 +++++------ tests/Feature/Http/CompanyTest.php | 22 +++++------ tests/Feature/Http/FileUploadTest.php | 20 +++++----- tests/Feature/Http/PersonnelTest.php | 24 ++++++------ tests/Feature/Http/StakeholderTestCase.php | 27 ++++++------- 11 files changed, 121 insertions(+), 128 deletions(-) diff --git a/routes/base.php b/routes/base.php index 43bf76c..3fcdf6d 100644 --- a/routes/base.php +++ b/routes/base.php @@ -21,14 +21,14 @@ Route::middleware(['api', 'auth:sanctum'])->group(function () { Route::apiResource('companies', Controllers\CompanyController::class); - // Route::prefix('companies')->controller(Controllers\CompanyController::class)->group(function () { - // Route::put('{company}/restore', 'restore')->name('companies.restore')->withTrashed(); - // }); + Route::prefix('companies')->controller(Controllers\CompanyController::class)->group(function () { + Route::put('{company}/restore', 'restore')->name('companies.restore')->withTrashed(); + }); Route::apiResource('personnels', Controllers\PersonnelController::class); - // Route::prefix('personnels')->controller(Controllers\EmployeeController::class)->group(function () { - // Route::put('{employee}/restore', 'restore')->name('personnels.restore')->withTrashed(); - // }); + Route::prefix('personnels')->controller(Controllers\PersonnelController::class)->group(function () { + Route::put('{personnel}/restore', 'restore')->name('personnels.restore')->withTrashed(); + }); Route::apiResource('addresses', Controllers\AddressController::class); Route::prefix('addresses')->controller(Controllers\AddressController::class)->group(function () { @@ -63,8 +63,8 @@ Route::apiResource($route, Controllers\StakeholderController::class) ->parameter((string) $route, 'stakeholder'); - // Route::prefix($route)->controller(Controllers\StakeholderController::class)->group(function () use ($route) { - // Route::put('{stakeholder}/restore', 'restore')->name($route.'.restore')->withTrashed(); - // }); + Route::prefix($route)->controller(Controllers\StakeholderController::class)->group(function () use ($route) { + Route::put('{stakeholder}/restore', 'restore')->name($route.'.restore')->withTrashed(); + }); } }); diff --git a/src/Http/Controllers/CompanyController.php b/src/Http/Controllers/CompanyController.php index baafe51..cb66888 100644 --- a/src/Http/Controllers/CompanyController.php +++ b/src/Http/Controllers/CompanyController.php @@ -22,47 +22,47 @@ public function __construct() /** * Index endpoint for companies. */ - public function index(Company $org, IndexRequest $request): OrganizationCollection + public function index(Company $company, IndexRequest $request): OrganizationCollection { return new OrganizationCollection( - $request->fulfill($org)->paginate() + $request->fulfill($company)->paginate() ); } /** * Create endpoint for company. */ - public function store(Company $org, StoreRequest $request): JsonResponse + public function store(Company $company, StoreRequest $request): JsonResponse { - $org = $request->fulfill($org); + $company = $request->fulfill($company); - return $this->show($org)->toResponse($request)->setStatusCode(201); + return $this->show($company)->toResponse($request)->setStatusCode(201); } /** * Show endpoint for a single company. */ - public function show(Company $org): OrganizationResource + public function show(Company $company): OrganizationResource { - return new OrganizationResource($org); + return new OrganizationResource($company); } /** * Update endpoint for a single company. */ - public function update(Company $org, UpdateRequest $request): OrganizationResource + public function update(Company $company, UpdateRequest $request): OrganizationResource { - $request->fulfill($org); + $request->fulfill($company); - return $this->show($org); + return $this->show($company); } /** * Delete endpoint for a single company. */ - public function destroy(Company $org, DeleteRequest $request): Response + public function destroy(Company $company, DeleteRequest $request): Response { - $request->fulfill($org); + $request->fulfill($company); return response()->noContent(); } @@ -70,10 +70,10 @@ public function destroy(Company $org, DeleteRequest $request): Response /** * Restore endpoint for a single company. */ - public function restore(Company $org): OrganizationResource + public function restore(Company $company): OrganizationResource { - $org->restore(); + $company->restore(); - return $this->show($org); + return $this->show($company->refresh()); } } diff --git a/src/Http/Controllers/PersonnelController.php b/src/Http/Controllers/PersonnelController.php index 7970b2d..d4ed970 100644 --- a/src/Http/Controllers/PersonnelController.php +++ b/src/Http/Controllers/PersonnelController.php @@ -36,35 +36,35 @@ public function index(Company $company, IndexRequest $request): PersonCollection public function store(Company $company, StoreRequest $request): JsonResponse { /** @var Personnel */ - $person = $request->fulfill($company); + $personnel = $request->fulfill($company); - return $this->show($person)->toResponse($request)->setStatusCode(201); + return $this->show($personnel)->toResponse($request)->setStatusCode(201); } /** * Show endpoint for a single employee. */ - public function show(Personnel $person): PersonResource + public function show(Personnel $personnel): PersonResource { - return new PersonResource($person); + return new PersonResource($personnel); } /** * Update endpoint for a single employee. */ - public function update(Personnel $person, UpdateRequest $request): PersonResource + public function update(Personnel $personnel, UpdateRequest $request): PersonResource { - $request->fulfill($person); + $request->fulfill($personnel); - return $this->show($person); + return $this->show($personnel); } /** * Delete endpoint for a single employee. */ - public function destroy(Personnel $person, DeleteRequest $request): Response + public function destroy(Personnel $personnel, DeleteRequest $request): Response { - $request->fulfill($person); + $request->fulfill($personnel); return response()->noContent(); } @@ -72,10 +72,10 @@ public function destroy(Personnel $person, DeleteRequest $request): Response /** * Restore endpoint for a single employee. */ - public function restore(Personnel $person): PersonResource + public function restore(Personnel $personnel): PersonResource { - $person->restore(); + $personnel->restore(); - return $this->show($person); + return $this->show($personnel); } } diff --git a/src/Http/Resources/AsAddress.php b/src/Http/Resources/AsAddress.php index 814aa78..14e3b2c 100644 --- a/src/Http/Resources/AsAddress.php +++ b/src/Http/Resources/AsAddress.php @@ -16,10 +16,6 @@ final protected function addressMeta(): array final protected function forAddress(Address $address): array { - if (! $address->exists) { - return []; - } - return [ $address->getKeyName() => $address->getKey(), 'type' => $address->type ? [ diff --git a/src/Repository.php b/src/Repository.php index b899b57..fffadd3 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -28,46 +28,59 @@ public function __construct(protected Router $router) /** * @param Authenticatable|\Illuminate\Foundation\Auth\User|HasProfile $user */ - public function resolvePerson(Authenticatable $user): Personnel + public function resolvePerson(Authenticatable $user, $default = null): Entity|Personnel { $user->loadMissing('profile'); - $key = $this->router->input('personnel'); + if (! ($key = $this->router->input('personnel', $default))) { + return $user->profile; + } + + /** @var \Creasi\Base\Database\Models\Person */ + $model = $user->profile()->newModelInstance(); - return $key ? $user->profile->resolveRouteBinding($key) : $user->profile; + return $this->router->current()->allowsTrashedBindings() + ? $model->resolveSoftDeletableRouteBinding($key) + : $model->resolveRouteBinding($key); } /** * @param Authenticatable|\Illuminate\Foundation\Auth\User|HasProfile $user */ - public function resolveOrganization(Authenticatable $user): Company + public function resolveOrganization(Authenticatable $user, $default = null): Entity|Company { $user->loadMissing('profile'); - $key = $this->router->input('company'); + if (! ($key = $this->router->input('company', $default))) { + return $user->profile->employer; + } + + /** @var \Creasi\Base\Database\Models\Organization */ + $model = $user->profile->employers()->newModelInstance(); - return $key ? $user->profile->employer->resolveRouteBinding($key) : $user->profile->employer; + return $this->router->current()->allowsTrashedBindings() + ? $model->resolveSoftDeletableRouteBinding($key) + : $model->resolveRouteBinding($key); } - public function resolveEntity(Company $org, Personnel $person): Entity + public function resolveEntity(Authenticatable $user): Entity { - $entity = $this->currentRoutePrefix('companies') ? $org : $person; $key = $this->router->input('entity'); - // For some reason we do need to resolve the binding ourselves - // see: https://stackoverflow.com/a/76717314/881743 - return $key ? $entity->resolveRouteBinding($key) : $entity; + return $this->currentRoutePrefix('companies') + ? $this->resolveOrganization($user, $key) + : $this->resolvePerson($user, $key); } - public function resolveStakeholder(Company $org, StakeholderType $type): Stakeholder + public function resolveStakeholder(Company $company, StakeholderType $type): Stakeholder { /** @var \Creasi\Base\Database\Models\OrganizationRelative */ - $relative = $org->stakeholders()->newQuery()->with('stakeholder')->where([ + $relative = $company->stakeholders()->newQuery()->with('stakeholder')->where([ 'type' => $type, 'stakeholder_id' => (int) $this->router->input('stakeholder'), ])->first(); - return $relative?->stakeholder ?: $org->newInstance(); + return $relative?->stakeholder ?: $company->newInstance(); } public function resolveOrganizationRelativeType(): StakeholderType diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index b2d9edb..f9a6931 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -29,7 +29,7 @@ class ServiceProvider extends IlluminateServiceProvider protected $policies = [ Contracts\Company::class => Policies\OrganizationPolicy::class, Contracts\Personnel::class => Policies\PersonPolicy::class, - Contracts\Stakeholder::class => Policies\StakeholderPolicy::class, + // Contracts\Stakeholder::class => Policies\StakeholderPolicy::class, Models\Address::class => Policies\AddressPolicy::class, Models\File::class => Policies\FilePolicy::class, ]; @@ -144,17 +144,9 @@ protected function defineRoutes(): void return $request->isMethodCacheable() ? $request->query('api_token') : null; }); - Route::bind('company', function (string $value) { - return app(Contracts\Company::class)->resolveRouteBinding($value); - }); - - Route::bind('personnel', function (string $value) { - return app(Contracts\Personnel::class)->resolveRouteBinding($value); - }); - - Route::bind('stakeholder', function (string $value) { - return app(Contracts\Stakeholder::class)->resolveRouteBinding($value); - }); + Route::bind('company', fn () => app(Contracts\Company::class)); + Route::bind('personnel', fn () => app(Contracts\Personnel::class)); + Route::bind('stakeholder', fn () => app(Contracts\Stakeholder::class)); if (app()->routesAreCached() || config('creasi.base.routes_enable') === false) { return; diff --git a/tests/Feature/Http/AddressTest.php b/tests/Feature/Http/AddressTest.php index 28eac89..22c7d93 100644 --- a/tests/Feature/Http/AddressTest.php +++ b/tests/Feature/Http/AddressTest.php @@ -102,7 +102,7 @@ public function should_able_to_update_existing_data(): void } #[Test] - public function should_able_to_delete_existing_data(): void + public function should_able_to_delete_and_restore_data(): void { Sanctum::actingAs($user = $this->user()); @@ -112,24 +112,19 @@ public function should_able_to_delete_existing_data(): void $response = $this->deleteJson($this->getRoutePath($model)); $response->assertNoContent(); - } - #[Test] - public function should_able_to_restore_deleted_data(): void - { - Sanctum::actingAs($user = $this->user()); + $this->assertSoftDeleted($model); - $model = Address::factory()->createOne(); - $user->profile->addresses()->save($model); + $response = $this->putJson($this->getRoutePath($model, 'restore')); - $deleted = clone $model; - $model->delete(); + $response->assertOk(); - $response = $this->putJson($this->getRoutePath($deleted, 'restore')); + $response = $this->deleteJson($this->getRoutePath($model), ['force' => true]); - $response->assertOk()->assertJsonStructure([ - 'data' => $this->dataStructure, - 'meta' => ['types'], + $response->assertNoContent(); + + $this->assertDatabaseMissing($model, [ + $model->getRouteKeyName() => $model->getRouteKey(), ]); } } diff --git a/tests/Feature/Http/CompanyTest.php b/tests/Feature/Http/CompanyTest.php index b7e3343..8f770cc 100644 --- a/tests/Feature/Http/CompanyTest.php +++ b/tests/Feature/Http/CompanyTest.php @@ -181,7 +181,7 @@ public function should_able_to_update_existing_data(): void } #[Test] - public function should_able_to_delete_existing_data(): void + public function should_able_to_delete_and_restore_data(): void { Sanctum::actingAs($this->user()); @@ -190,18 +190,8 @@ public function should_able_to_delete_existing_data(): void $response = $this->deleteJson($this->getRoutePath($model)); $response->assertNoContent(); - } - - #[Test] - public function should_able_to_restore_deleted_data(): void - { - $this->markTestIncomplete(); - Sanctum::actingAs($user = $this->user()); - - $model = Organization::factory()->createOne(); - - $model->delete(); + $this->assertSoftDeleted($model); $response = $this->putJson($this->getRoutePath($model, 'restore')); @@ -209,5 +199,13 @@ public function should_able_to_restore_deleted_data(): void 'data' => $this->dataStructure, 'meta' => [], ]); + + $response = $this->deleteJson($this->getRoutePath($model), ['force' => true]); + + $response->assertNoContent(); + + $this->assertDatabaseMissing($model, [ + $model->getRouteKeyName() => $model->getRouteKey(), + ]); } } diff --git a/tests/Feature/Http/FileUploadTest.php b/tests/Feature/Http/FileUploadTest.php index dc5adab..5bc08fd 100644 --- a/tests/Feature/Http/FileUploadTest.php +++ b/tests/Feature/Http/FileUploadTest.php @@ -77,7 +77,7 @@ public function should_able_to_update_existing_data(): void } #[Test] - public function should_able_to_delete_existing_data(): void + public function should_able_to_delete_and_restore_data(): void { Sanctum::actingAs($user = $this->user()); @@ -86,19 +86,19 @@ public function should_able_to_delete_existing_data(): void $response = $this->deleteJson($this->getRoutePath($model)); $response->assertNoContent(); - } - - #[Test] - public function should_able_to_restore_deleted_data(): void - { - Sanctum::actingAs($user = $this->user()); - $model = $user->profile->storeFile(FileType::Document, '/doc/file.pdf', 'document'); - - $model->delete(); + $this->assertSoftDeleted($model); $response = $this->putJson($this->getRoutePath($model, 'restore')); $response->assertOk(); + + $response = $this->deleteJson($this->getRoutePath($model), ['force' => true]); + + $response->assertNoContent(); + + $this->assertDatabaseMissing($model, [ + $model->getRouteKeyName() => $model->getRouteKey(), + ]); } } diff --git a/tests/Feature/Http/PersonnelTest.php b/tests/Feature/Http/PersonnelTest.php index f3065c0..da8eb73 100644 --- a/tests/Feature/Http/PersonnelTest.php +++ b/tests/Feature/Http/PersonnelTest.php @@ -168,7 +168,7 @@ public function should_able_to_update_existing_data(): void } #[Test] - public function should_able_to_delete_existing_data(): void + public function should_able_to_delete_and_restore_data(): void { Sanctum::actingAs($this->user()); @@ -177,24 +177,22 @@ public function should_able_to_delete_existing_data(): void $response = $this->deleteJson($this->getRoutePath($model)); $response->assertNoContent(); - } - - #[Test] - public function should_able_to_restore_deleted_data(): void - { - $this->markTestIncomplete(); - - Sanctum::actingAs($user = $this->user()); - - $model = Person::factory()->createOne(); - $model->delete(); + $this->assertSoftDeleted($model); $response = $this->putJson($this->getRoutePath($model, 'restore')); - $response->dump()->assertOk()->assertJsonStructure([ + $response->assertOk()->assertJsonStructure([ 'data' => $this->dataStructure, 'meta' => [], ]); + + $response = $this->deleteJson($this->getRoutePath($model), ['force' => true]); + + $response->assertNoContent(); + + $this->assertDatabaseMissing($model, [ + $model->getRouteKeyName() => $model->getRouteKey(), + ]); } } diff --git a/tests/Feature/Http/StakeholderTestCase.php b/tests/Feature/Http/StakeholderTestCase.php index af33329..1653042 100644 --- a/tests/Feature/Http/StakeholderTestCase.php +++ b/tests/Feature/Http/StakeholderTestCase.php @@ -114,7 +114,7 @@ public function should_able_to_update_existing_data(): void } #[Test] - public function should_able_to_delete_existing_data(): void + public function should_able_to_delete_and_restore_data(): void { Sanctum::actingAs($user = $this->user()); @@ -125,23 +125,24 @@ public function should_able_to_delete_existing_data(): void $response = $this->deleteJson($this->getRoutePath($model)); $response->assertNoContent(); - } - #[Test] - public function should_able_to_restore_deleted_data(): void - { - $this->markTestIncomplete(); + // TODO: Make it happen + // $this->assertSoftDeleted($model); - Sanctum::actingAs($user = $this->user()); - - $model = Organization::factory()->createOne(); + $response = $this->putJson($this->getRoutePath($model, 'restore')); - $user->profile->employer->addStakeholder($this->getRelativeType(), $model); + $response->assertOk()->assertJsonStructure([ + 'data' => $this->dataStructure, + 'meta' => [], + ]); - $model->delete(); + $response = $this->deleteJson($this->getRoutePath($model), ['force' => true]); - $response = $this->putJson($this->getRoutePath($model, 'restore')); + $response->assertNoContent(); - $response->assertOk(); + // TODO: Make it happen + // $this->assertDatabaseMissing($model, [ + // $model->getRouteKeyName() => $model->getRouteKey() + // ]); } }