diff --git a/routes/api.php b/routes/api.php index dec06ba36..fa01dfe90 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ findModelOrFail(); + $repository = $request->repository()->allowToUpdate($request); + + return $repository->attach( + $request, $request->repositoryId, + collect(Arr::wrap($request->input($request->relatedRepository))) + ->map(fn ($relatedRepositoryId) => $this->initializePivot( + $request, $model->{$request->viaRelationship ?? $request->relatedRepository}(), $relatedRepositoryId + )) + ); + } + + /** + * Initialize a fresh pivot model for the relationship. + * + * @param RestifyRequest $request + * @param $relationship + * @return mixed + * @throws \Binaryk\LaravelRestify\Exceptions\Eloquent\EntityNotFoundException + * @throws \Binaryk\LaravelRestify\Exceptions\UnauthorizedException + */ + protected function initializePivot(RestifyRequest $request, $relationship, $relatedKey) + { + $parentKey = $request->repositoryId; + + $parentKeyName = $relationship->getParentKeyName(); + $relatedKeyName = $relationship->getRelatedKeyName(); + + if ($parentKeyName !== $request->model()->getKeyName()) { + $parentKey = $request->findModelOrFail()->{$parentKeyName}; + } + + if ($relatedKeyName !== ($request->newRelatedRepository()::newModel())->getKeyName()) { + $relatedKey = $request->findRelatedModelOrFail()->{$relatedKeyName}; + } + + ($pivot = $relationship->newPivot())->forceFill([ + $relationship->getForeignPivotKeyName() => $parentKey, + $relationship->getRelatedPivotKeyName() => $relatedKey, + ]); + + if ($relationship->withTimestamps) { + $pivot->forceFill([ + $relationship->createdAt() => new DateTime, + $relationship->updatedAt() => new DateTime, + ]); + } + + return $pivot; + } +} diff --git a/src/Http/Requests/InteractWithRepositories.php b/src/Http/Requests/InteractWithRepositories.php index 991f2cee9..7d2030908 100644 --- a/src/Http/Requests/InteractWithRepositories.php +++ b/src/Http/Requests/InteractWithRepositories.php @@ -152,6 +152,41 @@ public function findModelQuery($repositoryId = null, $uriKey = null) ); } + public function findModelOrFail($id = null) + { + if ($id) { + return $this->findModelQuery($id)->firstOrFail(); + } + + return once(function () { + return $this->findModelQuery()->firstOrFail(); + }); + } + + public function findRelatedModelOrFail() + { + return once(function () { + return $this->findRelatedQuery()->firstOrFail(); + }); + } + + public function findRelatedQuery($relatedRepository = null, $relatedRepositoryId = null) + { + return $this->repository($relatedRepository ?? request('relatedRepository'))::newModel() + ->newQueryWithoutScopes() + ->whereKey($relatedRepositoryId ?? request('relatedRepositoryId')); + } + + protected function findPivot(RestifyRequest $request, $model) + { + $pivot = $model->{$request->relatedRepository}()->getPivotAccessor(); + + return $model->{$request->viaRelationship}() + ->withoutGlobalScopes() + ->lockForUpdate() + ->findOrFail($request->relatedRepositoryId)->{$pivot}; + } + public function viaParentModel() { $parent = $this->repository($this->viaRepository); @@ -163,4 +198,21 @@ public function viaQuery() { return $this->viaParentModel()->{$this->viaRelationship}(); } + + /** + * Get a new instance of the "related" resource being requested. + * + * @return Repository + */ + public function newRelatedRepository() + { + $resource = $this->relatedRepository(); + + return new $resource($resource::newModel()); + } + + public function relatedRepository() + { + return Restify::repositoryForKey($this->relatedRepository); + } } diff --git a/src/Http/Requests/RepositoryAttachRequest.php b/src/Http/Requests/RepositoryAttachRequest.php new file mode 100644 index 000000000..972af0822 --- /dev/null +++ b/src/Http/Requests/RepositoryAttachRequest.php @@ -0,0 +1,7 @@ +success(); } + public function attach(RestifyRequest $request, $repositoryId, Collection $pivots) + { + DB::transaction(function () use ($request, $pivots) { + return $pivots->map(fn ($pivot) => $pivot->forceFill($request->except($request->relatedRepository))) + ->map(fn ($pivot) => $pivot->save()); + }); + + return $this->response() + ->data($pivots) + ->created(); + } + public function destroy(RestifyRequest $request, $repositoryId) { $status = DB::transaction(function () { diff --git a/tests/Controllers/RepositoryFilterControllerTest.php b/tests/Controllers/RepositoryFilterControllerTest.php index f5c535799..b4a24c6d7 100644 --- a/tests/Controllers/RepositoryFilterControllerTest.php +++ b/tests/Controllers/RepositoryFilterControllerTest.php @@ -16,7 +16,6 @@ public function test_can_get_available_filters() $response = $this ->withoutExceptionHandling() ->getJson('restify-api/posts/filters') - ->dump() ->assertStatus(200); $this->assertCount(3, $response->json('data')); @@ -39,7 +38,6 @@ public function test_the_boolean_filter_is_applied() $response = $this ->withoutExceptionHandling() ->getJson('restify-api/posts?filters='.$filters) - ->dump() ->assertStatus(200); $this->assertCount(1, $response->json('data')); diff --git a/tests/Controllers/RepositoryPivotControllerTest.php b/tests/Controllers/RepositoryPivotControllerTest.php new file mode 100644 index 000000000..997065ad0 --- /dev/null +++ b/tests/Controllers/RepositoryPivotControllerTest.php @@ -0,0 +1,66 @@ +mockUsers(2)->first(); + $company = factory(Company::class)->create(); + + $response = $this->postJson('restify-api/companies/'.$company->id.'/attach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ]) + ->assertStatus(201); + + $response->assertJsonFragment([ + 'company_id' => '1', + 'user_id' => $user->id, + 'is_admin' => true, + ]); + } + + public function test_attach_multiple_users_to_a_company() + { + $user = $this->mockUsers(2)->first(); + $company = factory(Company::class)->create(); + $usersFromCompany = $this->getJson('/restify-api/users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); + $this->assertCount(0, $usersFromCompany->json('data')); + + $response = $this->postJson('restify-api/companies/'.$company->id.'/attach/users', [ + 'users' => [1, 2], + 'is_admin' => true, + ]) + ->assertStatus(201); + + $response->assertJsonFragment([ + 'company_id' => '1', + 'user_id' => $user->id, + 'is_admin' => true, + ]); + + $usersFromCompany = $this->getJson('/restify-api/users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); + $this->assertCount(2, $usersFromCompany->json('data')); + } + + public function test_after_attach_a_user_to_company_number_of_users_increased() + { + $user = $this->mockUsers()->first(); + $company = factory(Company::class)->create(); + + $usersFromCompany = $this->getJson('/restify-api/users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); + $this->assertCount(0, $usersFromCompany->json('data')); + + $this->postJson('restify-api/companies/'.$company->id.'/attach/users', [ + 'users' => $user->id, + ]); + + $usersFromCompany = $this->getJson('/restify-api/users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); + $this->assertCount(1, $usersFromCompany->json('data')); + } +} diff --git a/tests/Factories/CompanyFactory.php b/tests/Factories/CompanyFactory.php new file mode 100644 index 000000000..e0e43d8f3 --- /dev/null +++ b/tests/Factories/CompanyFactory.php @@ -0,0 +1,21 @@ +define(Company::class, function (Faker $faker) { + return [ + 'name' => $faker->name, + ]; +}); diff --git a/tests/Fixtures/Company/Company.php b/tests/Fixtures/Company/Company.php new file mode 100644 index 000000000..d1ed1dc86 --- /dev/null +++ b/tests/Fixtures/Company/Company.php @@ -0,0 +1,23 @@ +belongsToMany(User::class, 'company_user', 'company_id', 'user_id') + ->withPivot([ + 'is_admin', + ]) + ->withTimestamps(); + } +} diff --git a/tests/Fixtures/Company/CompanyRepository.php b/tests/Fixtures/Company/CompanyRepository.php new file mode 100644 index 000000000..f6b59e693 --- /dev/null +++ b/tests/Fixtures/Company/CompanyRepository.php @@ -0,0 +1,10 @@ +hasMany(Post::class); } + public function companies() + { + return $this->belongsToMany(Company::class, 'company_user', 'user_id', 'company_id')->withPivot([ + 'is_admin', + ])->withTimestamps(); + } + /** * Set default test values. */ diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index c61a275a9..6c083d20e 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -5,6 +5,7 @@ use Binaryk\LaravelRestify\Exceptions\RestifyHandler; use Binaryk\LaravelRestify\LaravelRestifyServiceProvider; use Binaryk\LaravelRestify\Restify; +use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyRepository; use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post; use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostAuthorizeRepository; use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostMergeableRepository; @@ -178,6 +179,7 @@ public function loadRepositories() Restify::repositories([ UserRepository::class, PostRepository::class, + CompanyRepository::class, PostMergeableRepository::class, PostAuthorizeRepository::class, PostWithUnauthorizedFieldsRepository::class, diff --git a/tests/Migrations/2019_12_22_000005_create_Company_user__table.php b/tests/Migrations/2019_12_22_000005_create_Company_user__table.php new file mode 100644 index 000000000..84575f583 --- /dev/null +++ b/tests/Migrations/2019_12_22_000005_create_Company_user__table.php @@ -0,0 +1,33 @@ +foreignId('company_id')->constrained('companies'); + $table->foreignId('user_id')->constrained('users'); + $table->boolean('is_admin')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('company_user'); + } +} diff --git a/tests/Migrations/2019_12_22_000005_create_companies_table.php b/tests/Migrations/2019_12_22_000005_create_companies_table.php new file mode 100644 index 000000000..7c2c2886b --- /dev/null +++ b/tests/Migrations/2019_12_22_000005_create_companies_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('companies'); + } +}