diff --git a/src/Eager/RelatedCollection.php b/src/Eager/RelatedCollection.php index 2baf9b827..e5727186c 100644 --- a/src/Eager/RelatedCollection.php +++ b/src/Eager/RelatedCollection.php @@ -3,10 +3,13 @@ namespace Binaryk\LaravelRestify\Eager; use Binaryk\LaravelRestify\Fields\BelongsTo; +use Binaryk\LaravelRestify\Fields\BelongsToMany; use Binaryk\LaravelRestify\Fields\EagerField; use Binaryk\LaravelRestify\Fields\Field; +use Binaryk\LaravelRestify\Fields\MorphToMany; use Binaryk\LaravelRestify\Filters\SortableFilter; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; +use Binaryk\LaravelRestify\Repositories\Repository; use Illuminate\Support\Collection; class RelatedCollection extends Collection @@ -27,6 +30,13 @@ public function forEager(RestifyRequest $request): self ->unique('attribute'); } + public function forManyToManyRelations(RestifyRequest $request): self + { + return $this->filter(function ($field) { + return $field instanceof BelongsToMany || $field instanceof MorphToMany; + })->filter(fn (EagerField $field) => $field->authorize($request)); + } + public function mapIntoSortable(RestifyRequest $request): self { return $this->filter(fn (EagerField $field) => $field->isSortable()) @@ -35,6 +45,28 @@ public function mapIntoSortable(RestifyRequest $request): self ->map(fn (BelongsTo $field) => SortableFilter::make()->usingBelongsTo($field)); } + public function forShow(RestifyRequest $request, Repository $repository): self + { + return $this->filter(function ($related) use ($request, $repository) { + if ($related instanceof Field) { + return $related->isShownOnShow($request, $repository); + } + + return $related; + }); + } + + public function forIndex(RestifyRequest $request, Repository $repository): self + { + return $this->filter(function ($related) use ($request, $repository) { + if ($related instanceof Field) { + return $related->isShownOnIndex($request, $repository); + } + + return $related; + }); + } + public function inRequest(RestifyRequest $request): self { return $this diff --git a/src/Fields/BelongsToMany.php b/src/Fields/BelongsToMany.php index 7e0c220d8..4d1adf896 100644 --- a/src/Fields/BelongsToMany.php +++ b/src/Fields/BelongsToMany.php @@ -4,6 +4,7 @@ use Binaryk\LaravelRestify\Contracts\RestifySearchable; use Binaryk\LaravelRestify\Fields\Concerns\Attachable; +use Binaryk\LaravelRestify\Repositories\PivotsCollection; use Binaryk\LaravelRestify\Repositories\Repository; use Closure; use Illuminate\Auth\Access\AuthorizationException; @@ -52,10 +53,10 @@ public function resolve($repository, $attribute = null) try { return $this->repositoryClass::resolveWith($item) ->allowToShow(app(Request::class)) - ->withExtraFields( - collect($this->pivotFields)->each(function (Field $field) use ($item) { - return $field->resolveCallback(fn () => $item->pivot->{$field->attribute}); - })->all() + ->withPivots( + PivotsCollection::make($this->pivotFields) + ->map(fn (Field $field) => clone $field) + ->resolveFromPivot($item->pivot) ) ->eagerState(); } catch (AuthorizationException $e) { diff --git a/src/Fields/Concerns/Attachable.php b/src/Fields/Concerns/Attachable.php index 511a35727..52c6d7398 100644 --- a/src/Fields/Concerns/Attachable.php +++ b/src/Fields/Concerns/Attachable.php @@ -3,12 +3,12 @@ namespace Binaryk\LaravelRestify\Fields\Concerns; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; +use Binaryk\LaravelRestify\Repositories\PivotsCollection; use Closure; use DateTime; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; trait Attachable { @@ -136,8 +136,8 @@ public function withPivot($fields) return $this; } - public function collectPivotFields(): Collection + public function collectPivotFields(): PivotsCollection { - return collect($this->pivotFields); + return PivotsCollection::make($this->pivotFields); } } diff --git a/src/Fields/EagerField.php b/src/Fields/EagerField.php index f95efe2ef..f529ce6bc 100644 --- a/src/Fields/EagerField.php +++ b/src/Fields/EagerField.php @@ -25,14 +25,6 @@ class EagerField extends Field */ public string $repositoryClass; - public function __construct($attribute, callable $resolveCallback = null) - { - parent::__construct($attribute, $resolveCallback); - - $this->showOnShow() - ->hideFromIndex(); - } - /** * Determine if the field should be displayed for the given request. * diff --git a/src/Fields/Field.php b/src/Fields/Field.php index fd42a11b5..443e54e7f 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -309,7 +309,7 @@ protected function fillAttributeFromValue(RestifyRequest $request, $model, $attr */ public function getAttribute() { - return $this->attribute; + return $this->label ?? $this->attribute; } /** @@ -381,6 +381,17 @@ public function messages(array $messages) return $this; } + public function serializeMessages(): array + { + $messages = []; + + foreach ($this->messages as $ruleFor => $message) { + $messages[$this->getAttribute().'.'.$ruleFor] = $message; + } + + return $messages; + } + public function getStoringRules(): array { return array_merge($this->rules, $this->storingRules); @@ -474,7 +485,7 @@ public function resolveForIndex($repository, $attribute = null) if ($attribute === 'Computed') { $this->value = call_user_func($this->computedCallback, $repository); - return; + return $this; } if (! $this->indexCallback) { @@ -494,7 +505,7 @@ public function resolve($repository, $attribute = null) if ($attribute === 'Computed') { $this->value = call_user_func($this->computedCallback, $repository); - return; + return $this; } if (! $this->resolveCallback) { diff --git a/src/Repositories/Concerns/InteractsWithAttachers.php b/src/Repositories/Concerns/InteractsWithAttachers.php index 623dd6ee8..1a8863059 100644 --- a/src/Repositories/Concerns/InteractsWithAttachers.php +++ b/src/Repositories/Concerns/InteractsWithAttachers.php @@ -10,9 +10,8 @@ trait InteractsWithAttachers { public function belongsToManyField(RestifyRequest $request): ?BelongsToMany { - return $request->newRepository() - ->collectFields($request) - ->filterForManyToManyRelations($request) + return $request->newRepository()::collectRelated() + ->forManyToManyRelations($request) ->firstWhere('attribute', $request->relatedRepository); } @@ -20,7 +19,7 @@ public function authorizeBelongsToMany(RestifyRequest $request): self { if (is_null($field = $this->belongsToManyField($request))) { $class = class_basename($request->repository()); - abort(400, "Missing BelongsToMany or MorphToMany field for [{$request->relatedRepository}]. This field should be in the [{$class}] class. Or you are not authorized to use that repository (see `allowRestify` policy method)."); + abort(400, "Missing BelongsToMany or MorphToMany field for [{$request->relatedRepository}]. This field should be in the related of the [{$class}] class. Or you are not authorized to use that repository (see `allowRestify` policy method)."); } $field->authorizeToAttach( diff --git a/src/Repositories/PivotsCollection.php b/src/Repositories/PivotsCollection.php new file mode 100644 index 000000000..4ef91e176 --- /dev/null +++ b/src/Repositories/PivotsCollection.php @@ -0,0 +1,17 @@ +map(function (Field $field) use ($pivot) { + return $field->resolveCallback(fn () => $pivot->{$field->attribute}); + }); + } +} diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 045fc79b0..aad44b8c5 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -5,6 +5,7 @@ use Binaryk\LaravelRestify\Contracts\RestifySearchable; use Binaryk\LaravelRestify\Controllers\RestResponse; use Binaryk\LaravelRestify\Eager\Related; +use Binaryk\LaravelRestify\Eager\RelatedCollection; use Binaryk\LaravelRestify\Exceptions\InstanceOfException; use Binaryk\LaravelRestify\Fields\BelongsToMany; use Binaryk\LaravelRestify\Fields\EagerField; @@ -162,6 +163,13 @@ abstract class Repository implements RestifySearchable, JsonSerializable */ public $extraFields = []; + /** + * A collection of pivots for the nested relationships. + * + * @var PivotsCollection + */ + private PivotsCollection $pivots; + public function __construct() { $this->bootIfNotBooted(); @@ -320,6 +328,20 @@ public function withExtraFields(array $fields): self return $this; } + public function withPivots(PivotsCollection $pivots): self + { + $this->pivots = $pivots; + + return $this; + } + + public function getPivots(): ?PivotsCollection + { + return isset($this->pivots) + ? $this->pivots + : null; + } + public function withResource($resource) { $this->resource = $resource; @@ -406,7 +428,7 @@ public function resolveShowAttributes(RestifyRequest $request) ->forShow($request, $this) ->filter(fn (Field $field) => $field->authorize($request)) ->when( - $this->eagerState, + $this->isEagerState(), function ($items) { return $items->filter(fn (Field $field) => ! $field instanceof EagerField); } @@ -502,10 +524,29 @@ public function resolveShowMeta($request) ]; } + public function resolveShowPivots(RestifyRequest $request): array + { + if (is_null($pivots = $this->getPivots())) { + return []; + } + + return $pivots + ->filter(fn (Field $field) => $field->authorize($request)) + ->each(fn (Field $field) => $field->resolve($this)) + ->map(fn (Field $field) => $field->serializeToValue($request)) + ->mapWithKeys(fn ($value) => $value) + ->all(); + } + + public function resolveIndexPivots(RestifyRequest $request): array + { + return $this->resolveShowPivots($request); + } + /** * Return a list with relationship for the current model. * - * @param $request + * @param RestifyRequest $request * @return array */ public function resolveRelationships($request): array @@ -515,6 +556,12 @@ public function resolveRelationships($request): array static::collectRelated() ->authorized($request) ->inRequest($request) + ->when($request->isShowRequest(), function (RelatedCollection $collection) use ($request) { + return $collection->forShow($request, $this); + }) + ->when($request->isForRepositoryRequest(), function (RelatedCollection $collection) use ($request) { + return $collection->forIndex($request, $this); + }) ->mapIntoRelated($request) ->each(function (Related $related) use ($request, $withs) { $relation = $related->getRelation(); @@ -749,7 +796,7 @@ public function attach(RestifyRequest $request, $repositoryId, Collection $pivot static::fillFields($request, $pivot, $fields); - $eagerField->authorizeToAttach($request, $pivot); + $eagerField->authorizeToAttach($request); return $pivot; })->each->save(); @@ -763,16 +810,10 @@ public function attach(RestifyRequest $request, $repositoryId, Collection $pivot public function detach(RestifyRequest $request, $repositoryId, Collection $pivots) { /** * @var BelongsToMany $eagerField */ - $eagerField = $request->newRepository() - ->collectFields($request) - ->filterForManyToManyRelations($request) + $eagerField = $request->newRepository()::collectRelated() + ->forManyToManyRelations($request) ->firstWhere('attribute', $request->relatedRepository); - if (is_null($eagerField)) { - $class = class_basename($request->repository()); - abort(400, "Missing BelongsToMany or MorphToMany field for [{$request->relatedRepository}]. This field should be in the [{$class}] class."); - } - $deleted = DB::transaction(function () use ($pivots, $eagerField, $request) { return $pivots ->map(fn ($pivot) => $eagerField->authorizeToDetach($request, $pivot) && $pivot->delete()); @@ -913,6 +954,7 @@ public function serializeForShow(RestifyRequest $request): array 'attributes' => $request->isShowRequest() ? $this->resolveShowAttributes($request) : $this->resolveIndexAttributes($request), 'relationships' => $this->when(value($related = $this->resolveRelationships($request)), $related), 'meta' => $this->when(value($meta = $request->isShowRequest() ? $this->resolveShowMeta($request) : $this->resolveIndexMeta($request)), $meta), + 'pivots' => $this->when(value($pivots = $this->resolveShowPivots($request)), $pivots), ]); } @@ -924,6 +966,7 @@ public function serializeForIndex(RestifyRequest $request): array 'attributes' => $this->when((bool) $attrs = $this->resolveIndexAttributes($request), $attrs), 'relationships' => $this->when(value($related = $this->resolveIndexRelationships($request)), $related), 'meta' => $this->when(value($meta = $this->resolveIndexMeta($request)), $meta), + 'pivots' => $this->when(value($pivots = $this->resolveIndexPivots($request)), $pivots), ]); } diff --git a/src/Repositories/ValidatingTrait.php b/src/Repositories/ValidatingTrait.php index 47d1352f8..45e26bb50 100644 --- a/src/Repositories/ValidatingTrait.php +++ b/src/Repositories/ValidatingTrait.php @@ -101,23 +101,14 @@ public static function validatorForAttach(RestifyRequest $request, $resource = n /** * @var Repository $on */ $on = $resource ?? static::resolveWith(static::newModel()); - /** - * @var BelongsToMany $field - */ - $pivotFields = $on - ->collectFields($request) - ->filterForManyToManyRelations($request) - ->firstWhere('attribute', $request->relatedRepository) - ->collectPivotFields(); - - $messages = $pivotFields->flatMap(function ($field) { - $messages = []; - foreach ($field->messages as $ruleFor => $message) { - $messages[$field->attribute.'.'.$ruleFor] = $message; - } + /** * @var BelongsToMany $field */ + $field = $on::collectRelated() + ->forManyToManyRelations($request) + ->firstWhere('attribute', $request->relatedRepository); - return $messages; - })->all(); + $pivotFields = $field->collectPivotFields(); + + $messages = $pivotFields->flatMap(fn (Field $field) => $field->serializeMessages())->all(); $rules = $pivotFields->mapWithKeys(function (Field $k) { return [ diff --git a/tests/Controllers/RepositoryAttachControllerTest.php b/tests/Controllers/RepositoryAttachControllerTest.php index 235fb589f..c496b2f53 100644 --- a/tests/Controllers/RepositoryAttachControllerTest.php +++ b/tests/Controllers/RepositoryAttachControllerTest.php @@ -2,106 +2,231 @@ namespace Binaryk\LaravelRestify\Tests\Controllers; +use Binaryk\LaravelRestify\Fields\BelongsToMany; +use Binaryk\LaravelRestify\Fields\Field; use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyPolicy; +use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyRepository; use Binaryk\LaravelRestify\Tests\Fixtures\User\User; +use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; +use Illuminate\Database\Eloquent\Relations\Pivot; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; class RepositoryAttachControllerTest extends IntegrationTest { - public function test_attach_a_user_to_a_company() + public function test_can_attach_repositories() { - $user = $this->mockUsers(2)->first(); + $user = $this->mockUsers()->first(); $company = factory(Company::class)->create(); - $response = $this->postJson('companies/'.$company->id.'/attach/users', [ + $this->postJson('companies/'.$company->id.'/attach/users', [ 'users' => $user->id, 'is_admin' => true, - ])->assertStatus(201); - - $response->assertJsonFragment([ + ])->assertCreated()->assertJsonFragment([ 'company_id' => '1', 'user_id' => $user->id, 'is_admin' => true, ]); + + $this->assertCount(1, Company::first()->users); } - public function test_pivot_field_validation() + public function test_cant_attach_repositories_not_authorized_to_attach() { - $user = $this->mockUsers(2)->first(); + Gate::policy(Company::class, CompanyPolicy::class); + + $user = $this->mockUsers()->first(); + $company = factory(Company::class)->create(); + + $this->authenticate( + factory(User::class)->create() + ); + + $_SERVER['allow_attach_users'] = false; + + $this->postJson('companies/'.$company->id.'/attach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ])->assertForbidden(); + + $_SERVER['allow_attach_users'] = true; + + $this->postJson('companies/'.$company->id.'/attach/users', [ + 'users' => $user->id, + 'is_admin' => true, + ])->assertCreated(); + + unset($_SERVER['allow_attach_users']); + } + + public function test_attach_pivot_field_validation() + { + $user = $this->mockUsers()->first(); $company = factory(Company::class)->create(); + CompanyRepository::partialMock() + ->shouldReceive('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class)->withPivot( + Field::make('is_admin')->rules('required')->messages([ + 'required' => $message = 'You should fill the is_admin information.', + ]) + ), + ]); + $this->postJson('companies/'.$company->id.'/attach/users', [ 'users' => $user->id, - ]) - ->assertStatus(400); + ])->assertStatus(400)->assertJsonFragment([ + 'is_admin' => [ + $message, + ], ]); + } + + public function test_pivot_field_present_when_show() + { + $company = tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach($this->mockUsers()->first()->id, [ + 'is_admin' => true, + ]); + $company->users()->attach($this->mockUsers()->first()->id); + }); + + $response = $this->getJson('companies/'.$company->id.'?related=users') + ->assertOk(); + + $this->assertSame( + true, + $response->json('data.relationships.users.0.pivots.is_admin') + ); + + $this->assertSame( + false, + $response->json('data.relationships.users.1.pivots.is_admin') + ); + } + + public function test_pivot_field_present_when_index() + { + tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach($this->mockUsers()->first()->id, [ + 'is_admin' => true, + ]); + $company->users()->attach($this->mockUsers()->first()->id); + }); + + $response = $this->getJson('companies?related=users') + ->assertOk(); + + $this->assertSame( + true, + $response->json('data.0.relationships.users.0.pivots.is_admin') + ); + $this->assertSame( + false, + $response->json('data.0.relationships.users.1.pivots.is_admin') + ); } public function test_attach_multiple_users_to_a_company() { $user = $this->mockUsers(2)->first(); $company = factory(Company::class)->create(); - $usersFromCompany = $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); - $this->assertCount(0, $usersFromCompany->json('data')); - $response = $this->postJson('companies/'.$company->id.'/attach/users', [ + $this->assertCount(0, $company->users); + + $this->postJson('companies/'.$company->id.'/attach/users', [ 'users' => [1, 2], 'is_admin' => true, - ]) - ->assertStatus(201); - - $response->assertJsonFragment([ + ])->assertCreated()->assertJsonFragment([ 'company_id' => '1', 'user_id' => $user->id, 'is_admin' => true, ]); - $usersFromCompany = $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); - $this->assertCount(2, $usersFromCompany->json('data')); + $this->assertCount(2, $company->fresh()->users); } - public function test_after_attach_a_user_to_company_number_of_users_increased() + public function test_many_to_many_field_can_intercept_attach_authorization() { $user = $this->mockUsers()->first(); $company = factory(Company::class)->create(); - $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users') - ->assertJsonCount(0, 'data'); + CompanyRepository::partialMock() + ->shouldReceive('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class) + ->canAttach(function ($request, $pivot) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(Pivot::class, $pivot); + + return false; + }), + ]); $this->postJson('companies/'.$company->id.'/attach/users', [ 'users' => $user->id, 'is_admin' => true, - ]); - - $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users') - ->assertJsonCount(1, 'data'); + ])->assertForbidden(); } - public function test_policy_to_attach_a_user_to_a_company() + public function test_many_to_many_field_can_intercept_attach_method() { - Gate::policy(Company::class, CompanyPolicy::class); - - $user = $this->mockUsers(2)->first(); + $user = $this->mockUsers()->first(); $company = factory(Company::class)->create(); - $this->authenticate( - factory(User::class)->create() - ); - $_SERVER['allow_attach_users'] = false; + CompanyRepository::partialMock() + ->shouldReceive('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class) + ->canAttach(function ($request, $pivot) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(Pivot::class, $pivot); + + return true; + }) + ->attachCallback(function ($request, $repository, $model) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(CompanyRepository::class, $repository); + $this->assertInstanceOf(Company::class, $model); + + $model->users()->attach($request->input('users')); + }), + ]); $this->postJson('companies/'.$company->id.'/attach/users', [ 'users' => $user->id, 'is_admin' => true, - ]) - ->assertForbidden(); + ])->assertOk(); - $_SERVER['allow_attach_users'] = true; + $this->assertCount(1, Company::first()->users); + } + + public function test_repository_can_intercept_attach() + { + $user = $this->mockUsers()->first(); + $company = factory(Company::class)->create(); + + CompanyRepository::partialMock()->shouldReceive('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class), + ]); + + CompanyRepository::$attachers = [ + 'users' => function ($request, $repository, $model) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(CompanyRepository::class, $repository); + $this->assertInstanceOf(Company::class, $model); + + $model->users()->attach($request->input('users')); + }, + ]; $this->postJson('companies/'.$company->id.'/attach/users', [ 'users' => $user->id, - 'is_admin' => true, - ]) - ->assertCreated(); + ])->assertOk(); + + $this->assertCount(1, $company->fresh()->users); } } diff --git a/tests/Controllers/RepositoryAttachInterceptorTest.php b/tests/Controllers/RepositoryAttachInterceptorTest.php deleted file mode 100644 index 92f89552b..000000000 --- a/tests/Controllers/RepositoryAttachInterceptorTest.php +++ /dev/null @@ -1,81 +0,0 @@ -create(); - $user = $this->mockUsers()->first(); - - $_SERVER['roles.canAttach.users'] = true; - - $this->postJson('roles/'.$role->id.'/attach/users', [ - 'users' => $user->id, - ])->assertCreated(); - - $this->assertDatabaseCount('model_has_roles', 1); - - $_SERVER['roles.canAttach.users'] = false; - - $this->postJson('roles/'.$role->id.'/attach/users', [ - 'users' => $user->id, - ])->assertForbidden(); - } - - public function test_attach_uses_field_resolver() - { - $this->mock(BelongsToMany::class); - - RoleRepository::partialMock() - ->expects('fields') - ->twice() - ->andReturn([ - field('name'), - BelongsToMany::new('users', 'users', UserRepository::class) - ->canAttach(function ($request, $pivot) { - $this->assertInstanceOf(RestifyRequest::class, $request); - $this->assertInstanceOf(Pivot::class, $pivot); - - return true; - }) - ->attachCallback(function ($request, $repository, Role $model) { - $this->assertInstanceOf(RestifyRequest::class, $request); - $this->assertInstanceOf(Repository::class, $repository); - $this->assertInstanceOf(Model::class, $model); - - $model->users()->attach($request->input('users')); - }), - ]); - - $role = factory(Role::class)->create(); - - $this->assertCount(0, $role->users()->get()); - - $user = $this->mockUsers()->first(); - - $this->postJson('roles/'.$role->id.'/attach/users', [ - 'users' => $user->id, - ])->assertSuccessful(); - - $this->assertCount(1, $role->users()->get()); - } -} diff --git a/tests/Controllers/RepositoryDetachControllerTest.php b/tests/Controllers/RepositoryDetachControllerTest.php index 3b3910661..5cfed3241 100644 --- a/tests/Controllers/RepositoryDetachControllerTest.php +++ b/tests/Controllers/RepositoryDetachControllerTest.php @@ -2,96 +2,138 @@ namespace Binaryk\LaravelRestify\Tests\Controllers; +use Binaryk\LaravelRestify\Fields\BelongsToMany; use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyPolicy; +use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyRepository; use Binaryk\LaravelRestify\Tests\Fixtures\User\User; +use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; +use Illuminate\Database\Eloquent\Relations\Pivot; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; class RepositoryDetachControllerTest extends IntegrationTest { - protected function setUp(): void + public function test_can_detach_repositories() { - parent::setUp(); - $_SERVER['roles.canDetach.users'] = true; - } - public function test_detach_a_user_from_a_company() - { - $user = $this->mockUsers(2)->first(); - $company = factory(Company::class)->create(); - $company->users()->attach($user->id); - $usersFromCompany = $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); - $this->assertCount(1, $usersFromCompany->json('data')); + $company = tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach($this->mockUsers()->first()->id, [ + 'is_admin' => true, + ]); + $company->users()->attach($this->mockUsers()->first()->id); + }); + + $this->assertCount(2, $company->users); + $this->postJson('companies/'.$company->id.'/detach/users', [ - 'users' => $user->id, - ])->assertStatus(204); + 'users' => [1, 2], + ])->assertNoContent(); - $usersFromCompany = $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); - $this->assertCount(0, $usersFromCompany->json('data')); + $this->assertCount(0, $company->fresh()->users); } - public function test_detach_multiple_users_from_a_company() + public function test_cant_detach_repositories_not_authorized_to_detach() { - $users = $this->mockUsers(3); - $company = factory(Company::class)->create(); - $company->users()->attach($users->pluck('id')); + Gate::policy(Company::class, CompanyPolicy::class); - $usersFromCompany = $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); - $this->assertCount(3, $usersFromCompany->json('data')); + $this->authenticate( + factory(User::class)->create() + ); + + $company = tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach($this->mockUsers()->first()->id, [ + 'is_admin' => true, + ]); + $company->users()->attach($this->mockUsers()->first()->id); + }); + + $_SERVER['allow_detach_users'] = false; $this->postJson('companies/'.$company->id.'/detach/users', [ 'users' => [1, 2], - ])->assertStatus(204); - - $usersFromCompany = $this->getJson('users?viaRepository=companies&viaRepositoryId=1&viaRelationship=users'); - $this->assertCount(1, $usersFromCompany->json('data')); + ])->assertForbidden(); } - public function test_forbidden_detach_users_from_company() + public function test_many_to_many_field_can_intercept_detach_authorization() { - $_SERVER['roles.canDetach.users'] = false; + CompanyRepository::partialMock() + ->shouldReceive('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class)->canDetach(function ($request, $pivot) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(Pivot::class, $pivot); - $users = $this->mockUsers(3); - $company = factory(Company::class)->create(); - $company->users()->attach($users->pluck('id')); + return false; + }), + ]); + + $company = tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach($this->mockUsers()->first()->id); + }); $this->postJson('companies/'.$company->id.'/detach/users', [ - 'users' => [1, 2], + 'users' => [1], ])->assertForbidden(); } - public function test_policy_to_detach_a_user_to_a_company() + public function test_many_to_many_field_can_intercept_detach_method() { - Gate::policy(Company::class, CompanyPolicy::class); + CompanyRepository::partialMock() + ->shouldReceive('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class)->detachCallback(function ($request, $repository, $model) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(CompanyRepository::class, $repository); + $this->assertInstanceOf(Company::class, $model); - $user = $this->mockUsers(2)->first(); - $company = factory(Company::class)->create(); - $this->authenticate( - factory(User::class)->create() - ); + $model->users()->detach($request->input('users')); - $this->postJson('companies/'.$company->id.'/attach/users', [ - 'users' => $user->id, - 'is_admin' => true, - ]) - ->assertCreated(); + return response()->noContent(); + }), + ]); - $_SERVER['allow_detach_users'] = false; + $company = tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach($this->mockUsers()->first()->id); + }); $this->postJson('companies/'.$company->id.'/detach/users', [ - 'users' => $user->id, - 'is_admin' => true, - ]) - ->assertForbidden(); + 'users' => [1], + ])->assertNoContent(); - $_SERVER['allow_detach_users'] = true; + $this->assertCount(0, $company->fresh()->users); + } + + public function test_repository_can_intercept_detach() + { + $mock = CompanyRepository::partialMock(); + $mock->shouldReceive('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class), + ]); + + CompanyRepository::$detachers = [ + 'users' => function ($request, $repository, $model) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(CompanyRepository::class, $repository); + $this->assertInstanceOf(Company::class, $model); + + $model->users()->detach($request->input('users')); + + return response()->noContent(); + }, + ]; + + $company = tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach($this->mockUsers()->first()->id); + }); $this->postJson('companies/'.$company->id.'/detach/users', [ - 'users' => $user->id, - 'is_admin' => true, - ]) - ->assertNoContent(); + 'users' => [1], + ])->assertNoContent(); + + $this->assertCount(0, $company->fresh()->users); } } diff --git a/tests/Controllers/RepositoryDetachInterceptorTest.php b/tests/Controllers/RepositoryDetachInterceptorTest.php deleted file mode 100644 index c0106de37..000000000 --- a/tests/Controllers/RepositoryDetachInterceptorTest.php +++ /dev/null @@ -1,118 +0,0 @@ -create(); - $user = $this->mockUsers()->first(); - $role->users()->attach($user->id); - - $_SERVER['roles.canDetach.users'] = true; - - $this->assertCount(1, $role->users()->get()); - - $this->postJson('roles/'.$role->id.'/detach/users', [ - 'users' => $user->id, - ])->assertSuccessful(); - - $this->assertCount(0, $role->users()->get()); - - $_SERVER['roles.canDetach.users'] = false; - - $this->postJson('roles/'.$role->id.'/detach/users', [ - 'users' => $user->id, - ])->assertForbidden(); - } - - public function test_detach_uses_field_resolver() - { - RoleRepository::partialMock() - ->expects('fields') - ->twice() - ->andReturn([ - field('name'), - BelongsToMany::new('users', 'users', UserRepository::class) - ->canDetach(function ($request, $pivot) { - $this->assertInstanceOf(RestifyRequest::class, $request); - $this->assertInstanceOf(Pivot::class, $pivot); - - return true; - }) - ->detachCallback(function ($request, $repository, Role $model) { - $this->assertInstanceOf(RestifyRequest::class, $request); - $this->assertInstanceOf(Repository::class, $repository); - $this->assertInstanceOf(Model::class, $model); - - $model->users()->detach($request->input('users')); - }), - ]); - - $role = factory(Role::class)->create(); - $user = $this->mockUsers()->first(); - $role->users()->attach($user->id); - - $this->assertCount(1, $role->users()->get()); - - $this->postJson('roles/'.$role->id.'/detach/users', [ - 'users' => $user->id, - ])->assertSuccessful(); - - $this->assertCount(0, $role->users()->get()); - } - - public function test_detach_uses_repository_resolver() - { - RoleRepository::partialMock() - ->expects('getDetachers') - ->twice() - ->andReturn([ - 'users' => function (RestifyRequest $request, RoleRepository $repository, Role $model) { - $this->assertInstanceOf(RestifyRequest::class, $request); - $this->assertInstanceOf(RoleRepository::class, $repository); - $this->assertInstanceOf(Role::class, $model); - - $model->users()->detach($request->input('users')); - }, - ]); - - $role = factory(Role::class)->create(); - $user = $this->mockUsers()->first(); - $role->users()->attach($user->id); - - $this->assertCount(1, $role->users()->get()); - - $this->postJson('roles/'.$role->id.'/detach/users', [ - 'users' => $user->id, - ])->assertSuccessful(); - - $this->assertCount(0, $role->users()->get()); - } -} diff --git a/tests/Controllers/RepositoryIndexControllerTest.php b/tests/Controllers/RepositoryIndexControllerTest.php index 3c7c1a3b2..67672daf3 100644 --- a/tests/Controllers/RepositoryIndexControllerTest.php +++ b/tests/Controllers/RepositoryIndexControllerTest.php @@ -122,7 +122,11 @@ public function test_using_custom_related_casts() public function test_repository_with_deep_relations() { - CompanyRepository::$related = ['users.posts']; + CompanyRepository::partialMock() + ->expects('related') + ->andReturn([ + 'users.posts', + ]); tap(factory(Company::class)->create(), function (Company $company) { tap($company->users()->create( diff --git a/tests/Fields/BelongsToManyFieldTest.php b/tests/Fields/BelongsToManyFieldTest.php index d516b0b98..1b84bb433 100644 --- a/tests/Fields/BelongsToManyFieldTest.php +++ b/tests/Fields/BelongsToManyFieldTest.php @@ -3,25 +3,14 @@ namespace Binaryk\LaravelRestify\Tests\Fields; use Binaryk\LaravelRestify\Fields\BelongsToMany; -use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; -use Binaryk\LaravelRestify\Repositories\Repository; -use Binaryk\LaravelRestify\Restify; use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; +use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyRepository; use Binaryk\LaravelRestify\Tests\Fixtures\User\User; use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; class BelongsToManyFieldTest extends IntegrationTest { - protected function setUp(): void - { - parent::setUp(); - - Restify::repositories([ - CompanyWithUsersRepository::class, - ]); - } - public function test_belongs_to_many_displays_on_relationships_show() { $company = tap(factory(Company::class)->create(), function (Company $company) { @@ -30,7 +19,7 @@ public function test_belongs_to_many_displays_on_relationships_show() ); }); - $this->get(CompanyWithUsersRepository::uriKey()."/{$company->id}?related=users") + $this->get(CompanyRepository::uriKey()."/{$company->id}?related=users") ->assertJsonStructure([ 'data' => [ 'relationships' => [ @@ -40,7 +29,7 @@ public function test_belongs_to_many_displays_on_relationships_show() ])->assertJsonCount(5, 'data.relationships.users'); } - public function test_can_hide_relationships() + public function test_belongs_to_many_can_hide_relationships_from_show() { $company = tap(factory(Company::class)->create(), function (Company $company) { $company->users()->attach( @@ -48,20 +37,67 @@ public function test_can_hide_relationships() ); }); - $_SERVER['hide_users_from_show'] = true; + CompanyRepository::partialMock() + ->expects('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class)->hideFromShow(), + ]); - $this->get(CompanyWithUsersRepository::uriKey()."/{$company->id}") + $this->get(CompanyRepository::uriKey()."/{$company->id}?related=users") ->assertJsonStructure([ 'data' => [], ])->assertJsonMissing([ - [ - 'relationships' => [ - 'users' => [], - ], ], + 'users', + ]); + } + + public function test_belongs_to_many_can_hide_relationships_from_index() + { + tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach( + factory(User::class)->create() + ); + }); + + CompanyRepository::partialMock() + ->expects('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class)->hideFromIndex(), ]); + + $this->get(CompanyRepository::uriKey().'?related=users')->assertJsonMissing([ + 'users', + ]); + + CompanyRepository::partialMock() + ->expects('related') + ->andReturn([ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class)->hideFromShow(), + ]); + + $this->get(CompanyRepository::uriKey().'?related=users')->assertJsonFragment([ + 'users', + ]); } - public function test_ignored_when_storing() + public function test_belongs_to_many_generates_nested_uri() + { + $company = tap(factory(Company::class)->create(), function (Company $company) { + $company->users()->attach( + factory(User::class)->create() + ); + }); + + $response = $this->get(CompanyRepository::uriKey()."/{$company->id}/users") + ->assertOk(); + + $this->assertSame( + 'users', + $response->json('data.0.type') + ); + } + + public function test_belongs_to_many_ignored_when_storing() { /** * @var User $user */ $user = factory(User::class)->create(); @@ -70,7 +106,7 @@ public function test_ignored_when_storing() $user->companies()->attach($companies); - $this->postJson(CompanyWithUsersRepository::uriKey(), [ + $this->postJson(CompanyRepository::uriKey(), [ 'name' => 'Binar Code', 'users' => [1, 2], ])->assertJsonMissing([ @@ -81,30 +117,3 @@ public function test_ignored_when_storing() ]); } } - -class CompanyWithUsersRepository extends Repository -{ - public static $model = Company::class; - - public static function getRelated() - { - return [ - 'users' => BelongsToMany::make('users', 'users', UserRepository::class) - ->hideFromShow(function () { - return $_SERVER['hide_users_from_show'] ?? false; - }), - ]; - } - - public function fields(RestifyRequest $request) - { - return [ - field('name'), - ]; - } - - public static function uriKey() - { - return 'companies-with-users-repository'; - } -} diff --git a/tests/Fixtures/Company/Company.php b/tests/Fixtures/Company/Company.php index d1ed1dc86..aabb248c8 100644 --- a/tests/Fixtures/Company/Company.php +++ b/tests/Fixtures/Company/Company.php @@ -15,6 +15,7 @@ class Company extends Model public function users() { return $this->belongsToMany(User::class, 'company_user', 'company_id', 'user_id') + ->using(CompanyUserPivot::class) ->withPivot([ 'is_admin', ]) diff --git a/tests/Fixtures/Company/CompanyRepository.php b/tests/Fixtures/Company/CompanyRepository.php index dc6d7a8d3..2ea77e8f6 100644 --- a/tests/Fixtures/Company/CompanyRepository.php +++ b/tests/Fixtures/Company/CompanyRepository.php @@ -12,18 +12,19 @@ class CompanyRepository extends Repository { public static $model = Company::class; - public static $related = [ - 'users', - ]; + public static function related(): array + { + return [ + 'users' => BelongsToMany::make('users', 'users', UserRepository::class)->withPivot( + Field::make('is_admin')->rules('required') + )->canDetach(fn ($request, $pivot) => isset($_SERVER['roles.canDetach.users']) && $_SERVER['roles.canDetach.users']), + ]; + } public function fields(RestifyRequest $request) { return [ field('name'), - - BelongsToMany::make('users', 'users', UserRepository::class)->withPivot( - Field::make('is_admin')->rules('required') - )->canDetach(fn ($request, $pivot) => $_SERVER['roles.canDetach.users']), ]; } } diff --git a/tests/Fixtures/Company/CompanyUserPivot.php b/tests/Fixtures/Company/CompanyUserPivot.php new file mode 100644 index 000000000..ae053f887 --- /dev/null +++ b/tests/Fixtures/Company/CompanyUserPivot.php @@ -0,0 +1,12 @@ + 'bool', + ]; +}