Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make scopes work with polymorphic relation directives #2110

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion src/Schema/Directives/MorphToDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string, mixed> $args
*
* @return array<int, int|string>
*/
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);
}
}
158 changes: 158 additions & 0 deletions tests/Integration/Schema/Directives/MorphToDirectiveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down