Skip to content

Commit

Permalink
Add @withoutGlobalScopes directive
Browse files Browse the repository at this point in the history
  • Loading branch information
saeed-rostami committed Jul 8, 2024
1 parent d542cbe commit f60fb9f
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

## v6.41.0

### Added

- Add `@withoutGlobalScopes` directive https://github.com/nuwave/lighthouse/pull/2577

## v6.40.0

### Added
Expand Down
42 changes: 42 additions & 0 deletions docs/6/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -4315,3 +4315,45 @@ type User {
```
If you just want to return the count itself as-is, use [@count](#count).
## @withoutGlobalScopes
```graphql
"""
Omit any number of global scopes from the query builder.

This directive should be used on arguments of type `Boolean`.
The scopes will be removed only if `true` is passed by the client.
"""
directive @withoutGlobalScopes(
"""
The names of the global scopes to omit.
"""
names: [String!]!
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
```
> This directive only works if the field resolver passes its builder through a call to `$resolveInfo->enhanceBuilder()`.
> Built-in field resolver directives that query the database do this, such as [@all](#all) or [@hasMany](#hasmany).
```php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
final class Post extends Model
{
protected static function booted(): void
{
self::addGlobalScope('scheduled', fn (Builder $query): Builder => $query
->whereNotNull('schedule_at'));
}
}
```
```graphql
type Query {
posts(
includeUnscheduled: Boolean @withoutGlobalScopes(names: ["scheduled"])
): [Post!]! @all
}
```
42 changes: 42 additions & 0 deletions docs/master/api-reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -4315,3 +4315,45 @@ type User {
```
If you just want to return the count itself as-is, use [@count](#count).
## @withoutGlobalScopes
```graphql
"""
Omit any number of global scopes from the query builder.

This directive should be used on arguments of type `Boolean`.
The scopes will be removed only if `true` is passed by the client.
"""
directive @withoutGlobalScopes(
"""
The names of the global scopes to omit.
"""
names: [String!]!
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
```
> This directive only works if the field resolver passes its builder through a call to `$resolveInfo->enhanceBuilder()`.
> Built-in field resolver directives that query the database do this, such as [@all](#all) or [@hasMany](#hasmany).
```php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
final class Post extends Model
{
protected static function booted(): void
{
self::addGlobalScope('scheduled', fn (Builder $query): Builder => $query
->whereNotNull('schedule_at'));
}
}
```
```graphql
type Query {
posts(
includeUnscheduled: Boolean @withoutGlobalScopes(names: ["scheduled"])
): [Post!]! @all
}
```
40 changes: 40 additions & 0 deletions src/Schema/Directives/WithoutGlobalScopesDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types=1);

namespace Nuwave\Lighthouse\Schema\Directives;

use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;

final class WithoutGlobalScopesDirective extends BaseDirective implements ArgBuilderDirective
{
public static function definition(): string
{
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Omit any number of global scopes from the query builder.
This directive should be used on arguments of type `Boolean`.
The scopes will be removed only if `true` is passed by the client.
"""
directive @withoutGlobalScopes(
"""
The names of the global scopes to omit.
"""
names: [String!]!
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
GRAPHQL;
}

public function handleBuilder(QueryBuilder|EloquentBuilder|Relation $builder, mixed $value): QueryBuilder|EloquentBuilder|Relation
{
if (! $value) {
return $builder;
}

$scopes = $this->directiveArgValue('names', $this->nodeName());

return $builder->withoutGlobalScopes($scopes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types=1);

namespace Tests\Integration\Schema\Directives;

use Illuminate\Support\Carbon;
use Tests\DBTestCase;
use Tests\Utils\Models\Podcast;

final class WithoutGlobalScopesDirectiveTest extends DBTestCase
{
public function testOmitsScopesWhenArgumentValueIsTrue(): void
{
$scheduledPodcast = factory(Podcast::class)->make();
assert($scheduledPodcast instanceof Podcast);
$scheduledPodcast->schedule_at = Carbon::tomorrow();
$scheduledPodcast->save();

$unscheduledPodcast = factory(Podcast::class)->make();
assert($unscheduledPodcast instanceof Podcast);
$unscheduledPodcast->schedule_at = null;
$unscheduledPodcast->save();

$this->schema = /** @lang GraphQL */ '
type Query {
podcasts(
includeUnscheduled: Boolean @withoutGlobalScopes(names: ["scheduled"])
): [Podcast!]! @all
}
type Podcast {
id: ID!
}
';

$this->graphQL(/** @lang GraphQL */ '
{
podcasts {
id
}
}
')->assertJsonCount(1, 'data.podcasts');

$this->graphQL(/** @lang GraphQL */ '
{
podcasts(includeUnscheduled: true) {
id
}
}
')->assertJsonCount(2, 'data.podcasts');
}
}
28 changes: 28 additions & 0 deletions tests/Utils/Models/Podcast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types=1);

namespace Tests\Utils\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

/**
* Primary key.
*
* @property int $id
*
* Attributes
* @property string $title
* @property \Illuminate\Support\Carbon|null $schedule_at
*
* Timestamps
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
*/
final class Podcast extends Model
{
protected static function booted(): void
{
self::addGlobalScope('scheduled', fn (Builder $query): Builder => $query
->whereNotNull('schedule_at'));
}
}
9 changes: 9 additions & 0 deletions tests/database/factories/PodcastFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

use Faker\Generator as Faker;

/** @var Illuminate\Database\Eloquent\Factory $factory */
$factory->define(Tests\Utils\Models\Podcast::class, static fn (Faker $faker): array => [
'title' => $faker->title,
'schedule_at' => $faker->randomElement([$faker->date(), null]),
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

final class CreateTestbenchPodcastsTable extends Migration
{
public function up(): void
{
Schema::create('podcasts', function (Blueprint $table): void {
$table->increments('id');
$table->string('title');
$table->timestamp('schedule_at')->nullable();
$table->softDeletes();
$table->timestamps();
});
}

public function down(): void
{
Schema::drop('podcasts');
}
}

0 comments on commit f60fb9f

Please sign in to comment.