diff --git a/src/Schema/Directives/MorphToDirective.php b/src/Schema/Directives/MorphToDirective.php index 80e8caebb0..cae2882a2b 100644 --- a/src/Schema/Directives/MorphToDirective.php +++ b/src/Schema/Directives/MorphToDirective.php @@ -2,6 +2,11 @@ namespace Nuwave\Lighthouse\Schema\Directives; +use Closure; +use GraphQL\Type\Definition\ResolveInfo; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\MorphTo; + class MorphToDirective extends RelationDirective { public static function definition(): string @@ -20,8 +25,77 @@ public static function definition(): string """ Apply scopes to the underlying query. """ - scopes: [String!] + scopes: [MorphToScopes!] ) on FIELD_DEFINITION + +""" +Options for the `scopes` argument on `@morphTo`. +""" +input MorphToScopes { + """ + Base or full class name of the related model the scope applies to. + """ + model: String! + + """ + Names of the scopes to apply. + """ + scopes: [String!]! +} GRAPHQL; } + + protected function scopes(): array + { + return []; + } + + protected function makeBuilderDecorator(ResolveInfo $resolveInfo): Closure + { + return function (object $builder) use ($resolveInfo) { + (parent::makeBuilderDecorator($resolveInfo))($builder); + + $scopes = []; + foreach ($this->directiveArgValue('scopes') ?? [] as $scopesForModel) { + $scopes[$this->namespaceModelClass($scopesForModel['model'])] = function (Builder $builder) use ($scopesForModel): void { + foreach ($scopesForModel['scopes'] as $scope) { + $builder->{$scope}(); + } + }; + } + + assert($builder instanceof MorphTo); + $builder->constrain($scopes); + }; + } + + /** + * @param array $args + * + * @return array + */ + protected function qualifyPath(array $args, ResolveInfo $resolveInfo): array + { + // Includes the field we are loading the relation for + $path = $resolveInfo->path; + + // In case we have no args, we can combine eager loads that are the same + if ([] === $args) { + array_pop($path); + } + + // Each relation must be loaded separately + $path[] = $this->relation(); + + $scopes = []; + foreach ($this->directiveArgValue('scopes') ?? [] as $scopesForModel) { + $scopes[] = $scopesForModel['model']; + foreach ($scopesForModel['scopes'] as $scope) { + $scopes[] = $scope; + } + } + + // Scopes influence the result of the query + return array_merge($path, $scopes); + } } diff --git a/tests/Integration/Schema/Directives/MorphToDirectiveTest.php b/tests/Integration/Schema/Directives/MorphToDirectiveTest.php index e6978075a2..183c052340 100644 --- a/tests/Integration/Schema/Directives/MorphToDirectiveTest.php +++ b/tests/Integration/Schema/Directives/MorphToDirectiveTest.php @@ -2,6 +2,7 @@ namespace Tests\Integration\Schema\Directives; +use Carbon\Carbon; use Tests\DBTestCase; use Tests\Utils\Models\Image; use Tests\Utils\Models\Post; @@ -126,6 +127,163 @@ public function testResolveMorphToWithCustomName(): void ]); } + public function testResolveMorphToWithScopes(): void + { + $user = factory(User::class)->create(); + assert($user instanceof User); + + $task = factory(Task::class)->make(); + assert($task instanceof Task); + $task->user()->associate($user); + $task->save(); + + $image = factory(Image::class)->make(); + assert($image instanceof Image); + $image->imageable()->associate($task); + $image->save(); + + $post = factory(Post::class)->make(); + assert($post instanceof Post); + $post->user()->associate($user->id); + $post->save(); + + $postImage = factory(Image::class)->make(); + assert($postImage instanceof Image); + $postImage->imageable()->associate($post); + $postImage->save(); + + $this->schema = /** @lang GraphQL */ ' + interface Imageable { + id: ID! + } + + type Task implements Imageable { + id: ID! + name: String! + } + + type Post implements Imageable { + id: ID! + title: String! + } + + type Image { + id: ID! + imageable: Imageable @morphTo(scopes: [ + { model: "Task", scopes: ["completed"] } + ]) + } + + type Query { + image ( + id: ID! @eq + ): Image @find + } + '; + + $this->graphQL(/** @lang GraphQL */ ' + query ($taskImage: ID!, $postImage: ID!){ + taskImage: image(id: $taskImage) { + id + imageable { + ... on Task { + id + name + } + ... on Post { + id + title + } + } + } + postImage: image(id: $postImage) { + id + imageable { + ... on Task { + id + name + } + ... on Post { + id + title + } + } + } + } + ', [ + 'taskImage' => $image->id, + 'postImage' => $postImage->id, + ])->assertJson([ + 'data' => [ + 'taskImage' => [ + 'id' => $image->id, + 'imageable' => null, + ], + 'postImage' => [ + 'id' => $postImage->id, + 'imageable' => [ + 'id' => $post->id, + 'title' => $post->title, + ], + ], + ], + ]); + + $task->completed_at = Carbon::now(); + $task->save(); + + $this->graphQL(/** @lang GraphQL */ ' + query ($taskImage: ID!, $postImage: ID!){ + taskImage: image(id: $taskImage) { + id + imageable { + ... on Task { + id + name + } + ... on Post { + id + title + } + } + } + postImage: image(id: $postImage) { + id + imageable { + ... on Task { + id + name + } + ... on Post { + id + title + } + } + } + } + ', [ + 'taskImage' => $image->id, + 'postImage' => $postImage->id, + ])->assertJson([ + 'data' => [ + 'taskImage' => [ + 'id' => $image->id, + 'imageable' => [ + 'id' => $task->id, + 'name' => $task->name, + ], + ], + 'postImage' => [ + 'id' => $postImage->id, + 'imageable' => [ + 'id' => $post->id, + 'title' => $post->title, + ], + ], + ], + ]); + } + public function testResolveMorphToUsingInterfaces(): void { $user = factory(User::class)->create();