Skip to content

Commit

Permalink
Make scopes work with polymorphic relation directives
Browse files Browse the repository at this point in the history
Resolves #2106
  • Loading branch information
spawnia committed Apr 7, 2022
1 parent c0acd3a commit b60c335
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 4 deletions.
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);
}
}
5 changes: 3 additions & 2 deletions src/Schema/Directives/RelationDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Execution\BatchLoader\BatchLoaderRegistry;
use Nuwave\Lighthouse\Execution\BatchLoader\RelationBatchLoader;
Expand Down Expand Up @@ -44,8 +45,8 @@ public function resolveField(FieldValue $fieldValue): FieldValue
$decorateBuilder = $this->makeBuilderDecorator($resolveInfo);
$paginationArgs = $this->paginationArgs($args);

/** @var \Illuminate\Database\Eloquent\Relations\Relation $relation */
$relation = $parent->{$relationName}();
assert($relation instanceof Relation);

// We can shortcut the resolution if the client only queries for a foreign key
// that we know to be present on the parent model.
Expand All @@ -71,7 +72,6 @@ public function resolveField(FieldValue $fieldValue): FieldValue
// Batch loading joins across both models, thus only works if they are on the same connection
&& $relation->getParent()->getConnectionName() === $relation->getRelated()->getConnectionName()
) {
/** @var \Nuwave\Lighthouse\Execution\BatchLoader\RelationBatchLoader $relationBatchLoader */
$relationBatchLoader = BatchLoaderRegistry::instance(
$this->qualifyPath($args, $resolveInfo),
function () use ($relationName, $decorateBuilder, $paginationArgs): RelationBatchLoader {
Expand All @@ -82,6 +82,7 @@ function () use ($relationName, $decorateBuilder, $paginationArgs): RelationBatc
return new RelationBatchLoader($modelsLoader);
}
);
assert($relationBatchLoader instanceof RelationBatchLoader);

return $relationBatchLoader->load($parent);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Schema/Directives/RelationDirectiveHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;

trait RelationDirectiveHelpers
Expand Down Expand Up @@ -33,10 +34,10 @@ protected function relation(): string
protected function makeBuilderDecorator(ResolveInfo $resolveInfo): Closure
{
return function (object $builder) use ($resolveInfo): void {
/** @var \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $builder */
if ($builder instanceof Relation) {
$builder = $builder->getQuery();
}
assert($builder instanceof Builder);

$resolveInfo->argumentSet->enhanceBuilder(
$builder,
Expand Down
83 changes: 83 additions & 0 deletions tests/Integration/Schema/Directives/MorphToDirectiveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Tests\Integration\Schema\Directives;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Tests\DBTestCase;
use Tests\Utils\Models\Image;
use Tests\Utils\Models\Post;
Expand Down Expand Up @@ -126,6 +128,87 @@ 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();

$this->schema = /** @lang GraphQL */ '
type Image {
id: ID!
imageable: Task @morphTo(scopes: [
{ model: "Task", scopes: ["completed"] }
])
}
type Task {
id: ID!
name: String!
}
type Query {
image (
id: ID! @eq
): Image @find
}
';

$this->graphQL(/** @lang GraphQL */ '
query ($id: ID!) {
image(id: $id) {
id
imageable {
id
}
}
}
', [
'id' => $image->id,
])->assertJson([
'data' => [
'image' => [
'id' => $image->id,
'imageable' => null,
],
],
]);

$task->completed_at = Carbon::now();
$task->save();

$this->graphQL(/** @lang GraphQL */ '
query ($id: ID!) {
image(id: $id) {
id
imageable {
id
}
}
}
', [
'id' => $image->id,
])->assertJson([
'data' => [
'image' => [
'id' => $image->id,
'imageable' => [
'id' => $task->id,
],
],
],
]);
}

public function testResolveMorphToUsingInterfaces(): void
{
$user = factory(User::class)->create();
Expand Down

0 comments on commit b60c335

Please sign in to comment.