diff --git a/UPGRADING.md b/UPGRADING.md index e2850966..438c4bd7 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,11 @@ # Upgrading +## From 7.3.1 to 7.3.2 + +## Breaking + +- The `$eagerState` repository property is now private, and it is of type `null|string` because it holds the parent repository that renders it. + ## From 6.x to 7.x High impact: diff --git a/src/Eager/Related.php b/src/Eager/Related.php index 72f7e0d1..f5417d66 100644 --- a/src/Eager/Related.php +++ b/src/Eager/Related.php @@ -3,6 +3,7 @@ namespace Binaryk\LaravelRestify\Eager; use Binaryk\LaravelRestify\Fields\EagerField; +use Binaryk\LaravelRestify\Filters\RelatedQuery; use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\Repositories\Repository; use Binaryk\LaravelRestify\Traits\HasColumns; @@ -36,6 +37,8 @@ class Related implements JsonSerializable */ private $resolverCallback; + public ?RelatedQuery $relatedQuery = null; + public function __construct(string $relation, EagerField $field = null) { $this->relation = $relation; @@ -67,7 +70,9 @@ public function resolveField(Repository $repository): EagerField public function resolve(RestifyRequest $request, Repository $repository): self { - $request->related()->resolved($repository::uriKey().$repository->getKey().$this->getRelation()); + $request->related()->resolved($this->uniqueIdentifierForRepository($repository)); + + ray($request->related()->resolvedRelationships); if (is_callable($this->resolverCallback)) { $this->value = call_user_func($this->resolverCallback, $request, $repository); @@ -129,6 +134,18 @@ public function resolveUsing(callable $resolver): self return $this; } + public function withRelatedQuery(RelatedQuery $relatedQuery): self + { + $this->relatedQuery = $relatedQuery; + + return $this; + } + + public function uniqueIdentifierForRepository(Repository $repository): string + { + return $repository::uriKey().$repository->getKey().$this->getRelation(); + } + #[ReturnTypeWillChange] public function jsonSerialize() { diff --git a/src/Eager/RelatedCollection.php b/src/Eager/RelatedCollection.php index d13d0889..d2426c90 100644 --- a/src/Eager/RelatedCollection.php +++ b/src/Eager/RelatedCollection.php @@ -22,7 +22,7 @@ public function intoAssoc(): self $mapKey = is_numeric($key) ? $value : $key; if ($value instanceof EagerField) { - $mapKey = $key ?: $value->getAttribute(); + $mapKey = (is_numeric($key) || empty($key)) ? $value->getAttribute() : $key; } return [ @@ -93,8 +93,30 @@ public function forIndex(RestifyRequest $request, Repository $repository): self public function inRequest(RestifyRequest $request, Repository $repository): self { - return $this->filter(function ($field, $key) use ($request, $repository) { - return $request->related()->hasRelation($repository::uriKey().'.'.$key); + return $this->filter(function (mixed $repositoryRelatedField, $repositoryRelatedKey) use ( + $request, + $repository + ) { + if ($repositoryRelatedField instanceof EagerField) { + if ($repository->getEagerParent()) { + $relatedKey = $repository->getEagerParent().'.'.$repositoryRelatedKey; + } elseif ($request->related()->rootKey === $repository::uriKey()) { + $relatedKey = $repository::uriKey().'.'.$repositoryRelatedKey; + } else { + $relatedKey = $repositoryRelatedKey; + } + + $relatedQuery = $request->related()->getRelatedQueryFor($relatedKey); + + if ($relatedQuery) { + $repositoryRelatedField->withRelatedQuery($relatedQuery); + } + + return (bool) $relatedQuery; + } + + // might be a closure or a normal relationship + return $request->related()->hasRelation($repository::uriKey().'.'.$repositoryRelatedKey); }); } @@ -144,10 +166,22 @@ public function forRequest(RestifyRequest $request, Repository $repository): sel public function unserialized(RestifyRequest $request, Repository $repository) { - return $this->filter(fn (Related $related) => ! in_array( - $repository::uriKey().$repository->getKey().$related->getRelation(), - $request->related()->resolvedRelationships, - true - )); + return $this->filter(function (Related $related) use ($request, $repository) { + return ! in_array( + $related->uniqueIdentifierForRepository($repository), + $request->related()->resolvedRelationships, + true + ); + }); + } + + public function markQuerySerialized(RestifyRequest $request, Repository $repository): self + { + return $this->each(function (Related $related) { +// dd($related->getValue()); + $related->relatedQuery?->serialized(); + + return $related; + }); } } diff --git a/src/Fields/EagerField.php b/src/Fields/EagerField.php index 321f9dac..3ae323dd 100644 --- a/src/Fields/EagerField.php +++ b/src/Fields/EagerField.php @@ -2,6 +2,7 @@ namespace Binaryk\LaravelRestify\Fields; +use Binaryk\LaravelRestify\Filters\RelatedQuery; use Binaryk\LaravelRestify\Repositories\Repository; use Binaryk\LaravelRestify\Restify; use Binaryk\LaravelRestify\Traits\HasColumns; @@ -29,6 +30,8 @@ class EagerField extends Field */ public string $repositoryClass; + private RelatedQuery $relatedQuery; + public function __construct($attribute, string $parentRepository = null) { parent::__construct(attribute: $attribute); @@ -40,7 +43,7 @@ public function __construct($attribute, string $parentRepository = null) } if (is_null($parentRepository)) { - $this->repositoryClass = tap(Restify::repositoryClassForKey($attribute), + $this->repositoryClass = tap(Restify::repositoryClassForKey(str($attribute)->pluralStudly()->kebab()->toString()), fn ($repository) => abort_unless($repository, 400, "Repository not found for the key [$attribute].")); } @@ -79,15 +82,16 @@ public function resolve($repository, $attribute = null) } try { - $this->value = $this->repositoryClass::resolveWith($relatedModel) + /** + * @var Repository $serializableRepository + */ + $serializableRepository = $this->repositoryClass::resolveWith($relatedModel); + + $this->value = $serializableRepository ->allowToShow(app(Request::class)) ->columns() ->eager($this); - } catch (AuthorizationException $e) { - if (is_null($relatedModel)) { - abort(403, 'You are not authorized to perform this action.'); - } - + } catch (AuthorizationException) { $class = get_class($relatedModel); $field = class_basename(get_called_class()); $policy = get_class(Gate::getPolicyFor($relatedModel)); @@ -128,4 +132,16 @@ public function getQualifiedKey( ): string { return $this->getRelation($repository)->getRelated()->getQualifiedKeyName(); } + + public function withRelatedQuery(RelatedQuery $relatedQuery): self + { + $this->relatedQuery = $relatedQuery; + + return $this; + } + + public function queryKeyThatRendered(): string + { + return $this->relatedQuery->relation; + } } diff --git a/src/Filters/RelatedDto.php b/src/Filters/RelatedDto.php index 55be6a7b..73bdadc2 100644 --- a/src/Filters/RelatedDto.php +++ b/src/Filters/RelatedDto.php @@ -18,6 +18,8 @@ class RelatedDto private bool $loaded = false; + public string $rootKey = ''; + public function __construct( ?RelatedQueryCollection $related = null, ) { @@ -125,6 +127,8 @@ public function makeTree(): array public function sync(Request $request, Repository $repository): self { + $this->rootKey = $repository::uriKey(); + if ($this->loaded) { return $this; } diff --git a/src/Repositories/Repository.php b/src/Repositories/Repository.php index 4506a1e2..2cf67e72 100644 --- a/src/Repositories/Repository.php +++ b/src/Repositories/Repository.php @@ -2,7 +2,6 @@ namespace Binaryk\LaravelRestify\Repositories; -use App\Restify\MediaRepository; use Binaryk\LaravelRestify\Actions\Action; use Binaryk\LaravelRestify\Contracts\RestifySearchable; use Binaryk\LaravelRestify\Eager\Related; @@ -164,11 +163,13 @@ class Repository implements RestifySearchable, JsonSerializable public static bool|array $public = false; /** - * Indicates if the repository is serializing for a eager relationship. + * Indicates if the repository is serializing for an eager relationship. * - * @var bool + * The $eagerState will reference to the parent that renders this via related. + * + * @var null|string */ - public bool $eagerState = false; + private null|string $eagerState = null; /** * Extra fields attached to the repository. Useful when display pivot fields. @@ -184,6 +185,8 @@ class Repository implements RestifySearchable, JsonSerializable */ private PivotsCollection $pivots; + private ?Repository $parentRepository = null; + public function __construct() { $this->bootIfNotBooted(); @@ -327,7 +330,7 @@ public function withResource($resource): self public static function resolveWith(Model $model): Repository { if (static::isMock()) { - return static::getMock()?->withResource($model); + return clone static::getMock()?->withResource($model); } return resolve(static::class)->withResource($model); @@ -393,12 +396,6 @@ public function resolveShowAttributes(RestifyRequest $request) $fields = $this->collectFields($request) ->forShow($request, $this) ->filter(fn (Field $field) => $field->authorize($request)) - ->when( - $this->isEagerState(), - function ($items) { - return $items->filter(fn (Field $field) => ! $field instanceof EagerField); - } - ) ->each(fn (Field $field) => $field->resolveForShow($this)) ->map(fn (Field $field) => $field->serializeToValue($request)) ->mapWithKeys(fn ($value) => $value) @@ -535,7 +532,7 @@ public function resolveRelationships($request): array return []; } - $related = static::collectRelated() + return static::collectRelated() ->forRequest($request, $this) ->mapIntoRelated($request, $this) ->unserialized($request, $this) @@ -548,26 +545,6 @@ public function resolveRelationships($request): array return $items; }) ->all(); - -// if (static::class === MediaRepository::class) { -// $related = static::collectRelated() -// ->intoAssoc() -// ->forRequest($request, $this) -// ->mapIntoRelated($request, $this) - //// ->unserialized($request, $this) -// ->map(fn (Related $related) => $related->resolve($request, $this)->getValue()) -// ->map(function (mixed $items) { -// if ($items instanceof Collection) { -// return $items->filter(); -// } -// -// return $items; -// }) -// ->all(); -// -// } - - return $related; } /** @@ -1148,6 +1125,7 @@ public function eager(EagerField $field = null): Repository return $this; } + $this->eagerState = $field->queryKeyThatRendered(); $this->columns($field->getColumns()); return $this; @@ -1155,7 +1133,12 @@ public function eager(EagerField $field = null): Repository public function isEagerState(): bool { - return $this->eagerState === true; + return ! is_null($this->eagerState); + } + + public function getEagerParent(): string + { + return $this->eagerState ?? ''; } public function restifyjsSerialize(RestifyRequest $request): array @@ -1187,4 +1170,16 @@ public static function isPublic(): bool { return static::$public; } + + public function withParentRepository(Repository $repository): self + { + $this->parentRepository = $repository; + + return $this; + } + + public function parentRepository(): ?Repository + { + return $this->parentRepository; + } } diff --git a/tests/Actions/FieldActionTest.php b/tests/Actions/FieldActionTest.php index b2b288e0..84b8cdae 100644 --- a/tests/Actions/FieldActionTest.php +++ b/tests/Actions/FieldActionTest.php @@ -76,18 +76,16 @@ public function handle(RestifyRequest $request, Post $post, int $row) Field::new('description')->action($action), ]); - $this - ->withoutExceptionHandling() - ->postJson(PostRepository::route('bulk'), [ - [ - 'title' => $title1 = 'First title', - 'description' => 'first description', - ], - [ - 'title' => $title2 = 'Second title', - 'description' => 'second description', - ], - ]) + $this->postJson(PostRepository::route('bulk'), [ + [ + 'title' => $title1 = 'First title', + 'description' => 'first description', + ], + [ + 'title' => $title2 = 'Second title', + 'description' => 'second description', + ], + ]) ->assertJson( fn (AssertableJson $json) => $json ->where('data.0.title', $title1) diff --git a/tests/Controllers/Index/IndexRelatedFeatureTest.php b/tests/Controllers/Index/IndexRelatedFeatureTest.php new file mode 100644 index 00000000..83f3c2f0 --- /dev/null +++ b/tests/Controllers/Index/IndexRelatedFeatureTest.php @@ -0,0 +1,232 @@ +shouldReceive('include') + ->andReturn([ + 'owner', + 'users' => HasMany::make('users', UserRepository::class), + 'extraData' => fn () => ['country' => 'Romania'], + 'extraMeta' => new InvokableExtraMeta, + ]); + + UserRepository::partialMock() + ->shouldReceive('include') + ->andReturn([ + 'posts' => HasMany::make('posts', PostRepository::class), + 'roles' => MorphToMany::make('roles', RoleRepository::class), + 'companies' => BelongsToMany::make('companies', CompanyRepository::class), + ]); + + Company::factory() + ->for(User::factory()->state([ + 'email' => 'owner@owner.com', + ]), 'owner') + ->has( + User::factory()->has( + Post::factory()->count(2) + )->has( + Role::factory() + ) + )->create(); + + $this->withoutExceptionHandling()->getJson(CompanyRepository::route(null, [ + 'related' => 'users.companies.users, users.posts, users.roles, extraData, extraMeta, owner', + ]))->assertJson( + fn (AssertableJson $json) => $json + ->where('data.0.type', 'companies') + ->has('data.0.relationships') + ->has('data.0.relationships.users') + ->where('data.0.relationships.users.0.type', 'users') + ->has('data.0.relationships.users.0.relationships.posts') + ->where('data.0.relationships.users.0.relationships.posts.0.type', 'posts') + ->where('data.0.relationships.users.0.relationships.roles.0.type', 'roles') + ->where('data.0.relationships.users.0.relationships.companies.0.type', 'companies') + ->where('data.0.relationships.extraData', ['country' => 'Romania']) + ->where('data.0.relationships.owner.email', 'owner@owner.com') + ->etc() + ); + } + + public function test_can_load_nested_parent_from_the_same_table(): void + { + CommentRepository::partialMock() + ->shouldReceive('include') + ->andReturn([ + BelongsTo::make('parent', CommentRepository::class), + HasMany::make('children', CommentRepository::class), + ]); + + $comment = Comment::factory() + ->state(['comment' => 'Root comment']) + ->for(Comment::factory()->state([ + 'comment' => 'Parent comment', + ]), 'parent') + ->has(Comment::factory()->state(['comment' => 'Children comments'])->count(2), 'children') + ->create(); + + $this->assertCount(2, $comment->children()->get()); + $this->assertModelExists($comment->parent()->first()); + + $this->withoutExceptionHandling(); + + $this->getJson(CommentRepository::route(query: [ + 'related' => 'parent, children', + ]))->assertJson(fn (AssertableJson $json) => $json + ->where('data.2.attributes.comment', 'Root comment') + ->has('data.2.relationships.parent') + ->missing('data.2.relationships.parent.relationships.parent') + ->missing('data.2.relationships.parent.relationships.children') + ->has('data.2.relationships.children') + ->count('data.2.relationships.children', 2) + ->missing('data.2.relationships.children.0.relationships.parent') + ->missing('data.2.relationships.children.1.relationships.parent') + ->where('data.0.attributes.comment', 'Children comments') + ->has('data.0.relationships.parent') + ->has('data.0.relationships.children') + ->count('data.0.relationships.children', 0) + ->etc() + ); + } + + public function test_index_related_doesnt_load_for_nested_relationships_that_didnt_require_it(): void + { + CommentRepository::partialMock() + ->shouldReceive('include') + ->andReturn([ + BelongsTo::make('user'), + BelongsTo::make('post'), + ]); + + PostRepository::partialMock() + ->shouldReceive('include') + ->andReturn([ + BelongsTo::make('user'), + ]); + + CommentFactory::many(); + + $this->getJson(CommentRepository::route(query: [ + 'related' => 'user, post.user', + ]))->assertJson( + fn (AssertableJson $json) => $json + ->where('data.0.id', '2') + ->has('data.0.relationships.user') + ->has('data.0.relationships.post') + ->has('data.0.relationships.post.relationships.user') + ->where('data.1.id', '1') + ->has('data.1.relationships.user') + ->has('data.1.relationships.post') + ->has('data.1.relationships.post.relationships.user') + ->etc() + ); + + app(RelatedDto::class)->reset(); + + $this->getJson(CommentRepository::route(query: [ + 'related' => 'user, post', + ]))->assertJson( + fn (AssertableJson $json) => $json + ->has('data.0.relationships.user') + ->has('data.0.relationships.post') + ->missing('data.0.relationships.post.relationships.user') + ->has('data.1.relationships.user') + ->has('data.1.relationships.post') + ->missing('data.1.relationships.post.relationships.user') + ->etc() + ); + } + + public function test_repository_can_resolve_related_using_callables(): void + { + PostRepository::$related = [ + 'user' => function ($request, $repository) { + $this->assertInstanceOf(Request::class, $request); + $this->assertInstanceOf(Repository::class, $repository); + + return 'foo'; + }, + ]; + + PostFactory::one(); + + $this->getJson(PostRepository::route(null, [ + 'related' => 'user', + ]))->assertJson( + fn (AssertableJson $json) => $json + ->where('data.0.relationships.user', 'foo') + ->etc() + ); + } + + /** * @test */ + public function it_can_paginate_keeping_relationships(): void + { + PostRepository::$related = [ + 'user', + ]; + + PostRepository::$sort = [ + 'id', + ]; + + PostFactory::many(5); + + Post::factory()->for(User::factory()->state([ + 'name' => $owner = 'John Doe', + ]))->create(); + + $this->getJson(PostRepository::route(null, [ + 'perPage' => 5, + 'related' => 'user', + 'sort' => 'id', + 'page' => 2, + ])) + ->assertJson( + fn (AssertableJson $json) => $json + ->count('data', 1) + ->where('data.0.relationships.user.name', $owner) + ->etc() + ); + } +} + +class InvokableExtraMeta +{ + public function __invoke() + { + return [ + 'userCount' => 10, + ]; + } +} diff --git a/tests/Controllers/Index/RepositoryIndexControllerTest.php b/tests/Controllers/Index/RepositoryIndexControllerTest.php index e49987b3..d75ef226 100644 --- a/tests/Controllers/Index/RepositoryIndexControllerTest.php +++ b/tests/Controllers/Index/RepositoryIndexControllerTest.php @@ -2,24 +2,14 @@ namespace Binaryk\LaravelRestify\Tests\Controllers\Index; -use Binaryk\LaravelRestify\Fields\BelongsToMany; -use Binaryk\LaravelRestify\Fields\HasMany; -use Binaryk\LaravelRestify\Fields\MorphToMany; -use Binaryk\LaravelRestify\Repositories\Repository; use Binaryk\LaravelRestify\Restify; use Binaryk\LaravelRestify\Tests\Database\Factories\PostFactory; -use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; -use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyRepository; use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post; use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostMergeableRepository; use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository; -use Binaryk\LaravelRestify\Tests\Fixtures\Role\Role; -use Binaryk\LaravelRestify\Tests\Fixtures\Role\RoleRepository; use Binaryk\LaravelRestify\Tests\Fixtures\User\User; -use Binaryk\LaravelRestify\Tests\Fixtures\User\UserRepository; use Binaryk\LaravelRestify\Tests\IntegrationTest; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Http\Request; use Illuminate\Testing\Fluent\AssertableJson; class RepositoryIndexControllerTest extends IntegrationTest @@ -157,108 +147,6 @@ public function it_can_return_related_entity(): void ); } - public function test_repository_can_resolve_related_using_callables(): void - { - PostRepository::$related = [ - 'user' => function ($request, $repository) { - $this->assertInstanceOf(Request::class, $request); - $this->assertInstanceOf(Repository::class, $repository); - - return 'foo'; - }, - ]; - - PostFactory::one(); - - $this->getJson(PostRepository::route(null, [ - 'related' => 'user', - ]))->assertJson( - fn (AssertableJson $json) => $json - ->where('data.0.relationships.user', 'foo') - ->etc() - ); - } - - public function test_can_retrieve_nested_relationships(): void - { - CompanyRepository::partialMock() - ->shouldReceive('include') - ->andReturn([ - 'owner', - 'users' => HasMany::make('users', UserRepository::class), - 'extraData' => fn () => ['country' => 'Romania'], - 'extraMeta' => new InvokableExtraMeta, - ]); - - UserRepository::partialMock() - ->shouldReceive('include') - ->andReturn([ - 'posts' => HasMany::make('posts', PostRepository::class), - 'roles' => MorphToMany::make('roles', RoleRepository::class), - 'companies' => BelongsToMany::make('companies', CompanyRepository::class), - ]); - - Company::factory() - ->for(User::factory()->state([ - 'email' => 'owner@owner.com', - ]), 'owner') - ->has( - User::factory()->has( - Post::factory()->count(2) - )->has( - Role::factory() - ) - )->create(); - - $this->withoutExceptionHandling()->getJson(CompanyRepository::route(null, [ - 'related' => 'users.companies.users, users.posts, users.roles, extraData, extraMeta, owner', - ]))->assertJson( - fn (AssertableJson $json) => $json - ->where('data.0.type', 'companies') - ->has('data.0.relationships') - ->has('data.0.relationships.users') - ->where('data.0.relationships.users.0.type', 'users') - ->has('data.0.relationships.users.0.relationships.posts') - ->where('data.0.relationships.users.0.relationships.posts.0.type', 'posts') - ->where('data.0.relationships.users.0.relationships.roles.0.type', 'roles') - ->where('data.0.relationships.users.0.relationships.companies.0.type', 'companies') - ->where('data.0.relationships.extraData', ['country' => 'Romania']) - ->where('data.0.relationships.owner.email', 'owner@owner.com') - ->etc() - ); - } - - /** * @test */ - public function it_can_paginate_keeping_relationships(): void - { - PostRepository::$related = [ - 'user', - ]; - - PostRepository::$sort = [ - 'id', - ]; - - PostFactory::many(5); - - Post::factory()->for(User::factory()->state([ - 'name' => $owner = 'John Doe', - ]))->create(); - - $this->getJson(PostRepository::route(null, [ - 'perPage' => 5, - 'related' => 'user', - 'sort' => 'id', - 'page' => 2, - ])) - ->assertJson( - fn (AssertableJson $json) => $json - ->count('data', 1) - ->where('data.0.relationships.user.name', $owner) - ->etc() - ); - } - public function test_index_unmergeable_repository_contains_only_explicitly_defined_fields(): void { PostFactory::one(); @@ -317,13 +205,3 @@ public function test_can_add_custom_index_main_meta_attributes(): void $this->assertEquals('Post Title', $response->json('meta.first_title')); } } - -class InvokableExtraMeta -{ - public function __invoke() - { - return [ - 'userCount' => 10, - ]; - } -} diff --git a/tests/Controllers/Show/ShowRelatedFeatureTest.php b/tests/Controllers/Show/ShowRelatedFeatureTest.php new file mode 100644 index 00000000..f8598265 --- /dev/null +++ b/tests/Controllers/Show/ShowRelatedFeatureTest.php @@ -0,0 +1,59 @@ +shouldReceive('include') + ->andReturn([ + BelongsTo::make('user'), + BelongsTo::make('post'), + ]); + + PostRepository::partialMock() + ->shouldReceive('include') + ->andReturn([ + BelongsTo::make('user'), + ]); + + $comment = CommentFactory::one(); + + $this->withoutExceptionHandling(); + + $this->getJson(CommentRepository::route($comment->id, [ + 'related' => 'user, post.user', + ]))->assertJson( + fn (AssertableJson $json) => $json + ->has('data.relationships.user') + ->has('data.relationships.post') + ->has('data.relationships.post.relationships.user') + ->etc() + ); + + app(RelatedDto::class)->reset(); + + $this->getJson(CommentRepository::route($comment->id, [ + 'related' => 'user, post', + ]))->assertJson( + fn (AssertableJson $json) => $json + ->has('data.relationships.user') + ->has('data.relationships.post') + ->missing('data.relationships.post.relationships.user') + ->etc() + ); + } +} diff --git a/tests/Feature/Filters/BelongsToFilterTest.php b/tests/Feature/Filters/BelongsToFilterTest.php index ef17d011..ffe3b787 100644 --- a/tests/Feature/Filters/BelongsToFilterTest.php +++ b/tests/Feature/Filters/BelongsToFilterTest.php @@ -57,6 +57,8 @@ public function test_can_sort_desc_using_belongs_to_field(): void ->etc() ); + app(RelatedDto::class)->reset(); + $this ->getJson(PostRepository::route(query: [ 'related' => 'user', @@ -109,8 +111,8 @@ public function test_can_sort_asc_using_belongs_to_field(): void 'perPage' => 5, ]))->assertJson( fn (AssertableJson $json) => $json - ->where('data.0.relationships.user.attributes.name', 'Ame') - ->etc() + ->where('data.0.relationships.user.attributes.name', 'Ame') + ->etc() ); $this @@ -121,8 +123,8 @@ public function test_can_sort_asc_using_belongs_to_field(): void 'page' => 4, ]))->assertJson( fn (AssertableJson $json) => $json - ->where('data.5.relationships.user.attributes.name', 'Zez') - ->etc() + ->where('data.5.relationships.user.attributes.name', 'Zez') + ->etc() ); } diff --git a/tests/Fixtures/Comment/Comment.php b/tests/Fixtures/Comment/Comment.php new file mode 100644 index 00000000..69f8ec1c --- /dev/null +++ b/tests/Fixtures/Comment/Comment.php @@ -0,0 +1,44 @@ +belongsTo(User::class); + } + + public function post(): BelongsTo + { + return $this->belongsTo(Post::class); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(static::class, 'parent_comment_id'); + } + + public function children(): HasMany + { + return $this->hasMany(static::class, 'parent_comment_id'); + } +} diff --git a/tests/Fixtures/Comment/CommentPolicy.php b/tests/Fixtures/Comment/CommentPolicy.php new file mode 100644 index 00000000..d6f0ccab --- /dev/null +++ b/tests/Fixtures/Comment/CommentPolicy.php @@ -0,0 +1,51 @@ +belongsTo(User::class); } + + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } } diff --git a/tests/Fixtures/User/User.php b/tests/Fixtures/User/User.php index 2f075007..04d3746b 100644 --- a/tests/Fixtures/User/User.php +++ b/tests/Fixtures/User/User.php @@ -3,11 +3,13 @@ namespace Binaryk\LaravelRestify\Tests\Fixtures\User; use Binaryk\LaravelRestify\Contracts\Sanctumable; +use Binaryk\LaravelRestify\Tests\Fixtures\Comment\Comment; use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; use Binaryk\LaravelRestify\Tests\Fixtures\Post\Post; use Binaryk\LaravelRestify\Tests\Fixtures\Role\Role; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Query\Builder; @@ -41,6 +43,7 @@ class User extends Authenticatable implements Sanctumable, MustVerifyEmail 'email', 'active', 'password', + 'creator_id', 'email_verified_at', 'avatar', 'created_at', @@ -118,4 +121,14 @@ public function profile() ], ]; } + + public function creator(): BelongsTo + { + return $this->belongsTo(static::class, 'creator_id'); + } + + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } } diff --git a/tests/Fixtures/User/UserRepository.php b/tests/Fixtures/User/UserRepository.php index 9ea8f91f..eeec5a25 100644 --- a/tests/Fixtures/User/UserRepository.php +++ b/tests/Fixtures/User/UserRepository.php @@ -27,6 +27,7 @@ class UserRepository extends Repository public static array $middleware = []; public static array $match = [ + 'id' => 'int', 'created_at' => RestifySearchable::MATCH_DATETIME, ]; diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 6b0a34a2..7800e431 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -9,6 +9,9 @@ use Binaryk\LaravelRestify\Restify; use Binaryk\LaravelRestify\RestifyApplicationServiceProvider; use Binaryk\LaravelRestify\Tests\Concerns\Mockers; +use Binaryk\LaravelRestify\Tests\Fixtures\Comment\Comment; +use Binaryk\LaravelRestify\Tests\Fixtures\Comment\CommentPolicy; +use Binaryk\LaravelRestify\Tests\Fixtures\Comment\CommentRepository; use Binaryk\LaravelRestify\Tests\Fixtures\Company\Company; use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyPolicy; use Binaryk\LaravelRestify\Tests\Fixtures\Company\CompanyRepository; @@ -104,6 +107,7 @@ public function repositories(): self CompanyRepository::class, PostWithHiddenFieldRepository::class, RoleRepository::class, + CommentRepository::class, ]); return $this; @@ -151,6 +155,7 @@ private function policies(): self Gate::policy(Company::class, CompanyPolicy::class); Gate::policy(Role::class, RolePolicy::class); Gate::policy(ActionLog::class, ActionLogPolicy::class); + Gate::policy(Comment::class, CommentPolicy::class); return $this; } diff --git a/tests/database/factories/CommentFactory.php b/tests/database/factories/CommentFactory.php new file mode 100644 index 00000000..7d1d86ff --- /dev/null +++ b/tests/database/factories/CommentFactory.php @@ -0,0 +1,34 @@ + User::factory(), + 'post_id' => Post::factory(), + 'parent_comment_id' => null, + 'comment' => $this->faker->sentence, + ]; + } + + public static function one(array $attributes = []): Comment + { + return Comment::factory()->create($attributes); + } + + public static function many(int $count = 2, array $attributes = []): Collection + { + return app(static::class)->count($count)->create($attributes); + } +} diff --git a/tests/database/factories/UserFactory.php b/tests/database/factories/UserFactory.php index 61956a26..ec8a19b7 100644 --- a/tests/database/factories/UserFactory.php +++ b/tests/database/factories/UserFactory.php @@ -20,6 +20,7 @@ public function definition(): array 'remember_token' => Str::random(10), 'created_at' => now(), 'updated_at' => now(), + 'creator_id' => null, ]; } diff --git a/tests/database/migrations/2017_10_10_000000_create_users_table.php b/tests/database/migrations/2017_10_10_000000_create_users_table.php index 847d905f..9be928a7 100644 --- a/tests/database/migrations/2017_10_10_000000_create_users_table.php +++ b/tests/database/migrations/2017_10_10_000000_create_users_table.php @@ -23,6 +23,7 @@ public function up() $table->string('password'); $table->boolean('active')->default(true); $table->timestamp('email_verified_at')->nullable(); + $table->foreignId('creator_id')->nullable()->comment('creator'); $table->rememberToken(); $table->timestamps(); $table->softDeletes(); diff --git a/tests/database/migrations/2022_08_28_000005_create_comments_table.php b/tests/database/migrations/2022_08_28_000005_create_comments_table.php new file mode 100644 index 00000000..3bf11b64 --- /dev/null +++ b/tests/database/migrations/2022_08_28_000005_create_comments_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('user_id')->index()->nullable(); + $table->foreignId('post_id')->index()->nullable(); + $table->foreignId('parent_comment_id')->nullable(); + $table->string('comment')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('comments'); + } +};