From 36b0ba5c5e2a4b5fc639bf22559d6b9359f186ab Mon Sep 17 00:00:00 2001 From: lorado Date: Tue, 27 Aug 2019 21:49:13 +0200 Subject: [PATCH 01/19] implement support for querying soft deleted elements --- CHANGELOG.md | 4 + docs/master/eloquent/getting-started.md | 26 ++++ src/Schema/AST/ASTBuilder.php | 21 ++++ src/Schema/Directives/AllDirective.php | 10 +- src/Schema/Directives/FindDirective.php | 10 +- src/Schema/Directives/PaginateDirective.php | 3 + src/Support/Utils.php | 43 +++++++ .../Schema/Directives/AllDirectiveTest.php | 100 +++++++++++++++ .../Schema/Directives/FindDirectiveTest.php | 114 ++++++++++++++++++ .../Directives/PaginateDirectiveTest.php | 111 +++++++++++++++++ 10 files changed, 436 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcae2eda91..d02b3784ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/nuwave/lighthouse/compare/v4.1.1...master) +### Added + +- Add support for defining `onlyTrashed`, `withTrashed`, and `withoutTrashed` on models with soft delete for `@all`, `@find` and `@paginate` directives + ## [4.1.1](https://github.com/nuwave/lighthouse/compare/v4.1.0...v4.1.1) ### Fixed diff --git a/docs/master/eloquent/getting-started.md b/docs/master/eloquent/getting-started.md index e6202e5b66..867af60e5c 100644 --- a/docs/master/eloquent/getting-started.md +++ b/docs/master/eloquent/getting-started.md @@ -249,3 +249,29 @@ This mutation will return the deleted object, so you will have a last chance to } } ``` + +## Soft delete + +If your model uses soft delete, you can define an attribute with enum type `Trash`, that is provided by lighthouse. It has `ONLY`, `WITH` and `WITHOUT` values, according laravels `onlyTrashed()`, `withTrashed()` and `withoutTrashed()` methods. + +You are free to choose an attribute name you like, as lightouse searches for attributes of type `Trash` automatically. + +Currently `@all`, `@paginate` and `@find` directives supports this feature + +For example your schema has following structure + +```graphql +type Query { + tasks(trashed: Trash): [Task!]! @all +} +``` + +Then you can make queries like this: + +```graphql +{ + tasks(trashed: WITH) { + ... + } +} +``` diff --git a/src/Schema/AST/ASTBuilder.php b/src/Schema/AST/ASTBuilder.php index 79b46e1284..4508f6eff4 100644 --- a/src/Schema/AST/ASTBuilder.php +++ b/src/Schema/AST/ASTBuilder.php @@ -95,6 +95,7 @@ public function build(): DocumentAST $this->addPaginationInfoTypes(); $this->addNodeSupport(); $this->addOrderByTypes(); + $this->addTrashEnum(); // Listeners may manipulate the DocumentAST that is passed by reference // into the ManipulateAST event. This can be useful for extensions @@ -361,4 +362,24 @@ enum SortOrder { ') ); } + + /** + * Add Trash enum that can be used in arguments with @paginate. + * + * @see \Nuwave\Lighthouse\Schema\Directives\PaginateDirective + * + * @return void + */ + protected function addTrashEnum(): void + { + $this->documentAST->setTypeDefinition( + PartialParser::enumTypeDefinition(' + enum Trash { + ONLY @enum(value: "only") + WITH @enum(value: "with") + WITHOUT @enum(value: "without") + } + ') + ); + } } diff --git a/src/Schema/Directives/AllDirective.php b/src/Schema/Directives/AllDirective.php index d43034e71f..4a9cdd99fe 100644 --- a/src/Schema/Directives/AllDirective.php +++ b/src/Schema/Directives/AllDirective.php @@ -7,6 +7,7 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; +use Nuwave\Lighthouse\Support\Utils; class AllDirective extends BaseDirective implements FieldResolver { @@ -33,7 +34,7 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) /** @var \Illuminate\Database\Eloquent\Model $modelClass */ $modelClass = $this->getModelClass(); - return $resolveInfo + $query = $resolveInfo ->builder ->addScopes( $this->directiveArgValue('scopes', []) @@ -41,8 +42,11 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) ->apply( $modelClass::query(), $args - ) - ->get(); + ); + + Utils::applyTrashedModificationIfNeeded($resolveInfo, $args, $query); + + return $query->get(); } ); } diff --git a/src/Schema/Directives/FindDirective.php b/src/Schema/Directives/FindDirective.php index 2303815fa7..638de3a948 100644 --- a/src/Schema/Directives/FindDirective.php +++ b/src/Schema/Directives/FindDirective.php @@ -8,6 +8,7 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; +use Nuwave\Lighthouse\Support\Utils; class FindDirective extends BaseDirective implements FieldResolver { @@ -34,7 +35,7 @@ public function resolveField(FieldValue $fieldValue): FieldValue return $fieldValue->setResolver( function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($model): ?Model { - $results = $resolveInfo + $query = $resolveInfo ->builder ->addScopes( $this->directiveArgValue('scopes', []) @@ -42,8 +43,11 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) ->apply( $model::query(), $args - ) - ->get(); + ); + + Utils::applyTrashedModificationIfNeeded($resolveInfo, $args, $query); + + $results = $query->get(); if ($results->count() > 1) { throw new Error('The query returned more than one result.'); diff --git a/src/Schema/Directives/PaginateDirective.php b/src/Schema/Directives/PaginateDirective.php index 4f6f0ac78c..3e7fad8546 100644 --- a/src/Schema/Directives/PaginateDirective.php +++ b/src/Schema/Directives/PaginateDirective.php @@ -18,6 +18,7 @@ use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Nuwave\Lighthouse\Support\Contracts\FieldManipulator; +use Nuwave\Lighthouse\Support\Utils; class PaginateDirective extends BaseDirective implements FieldResolver, FieldManipulator { @@ -87,6 +88,8 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) $args ); + Utils::applyTrashedModificationIfNeeded($resolveInfo, $args, $query); + if ($query instanceof ScoutBuilder) { return $query->paginate($first, 'page', $page); } diff --git a/src/Support/Utils.php b/src/Support/Utils.php index d15c004466..b73b37af96 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -3,6 +3,9 @@ namespace Nuwave\Lighthouse\Support; use Closure; +use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\ResolveInfo; use ReflectionClass; use ReflectionException; use Nuwave\Lighthouse\Exceptions\DefinitionException; @@ -79,4 +82,44 @@ public static function accessProtected($object, string $memberName, $default = n return $default; } } + + /** + * Apply withTrashed, onlyTrashed or withoutTrashed to given $query if needed. + * Resolve info is used to get list if argument definitions of current field. + * If there is any argument of enum type Trash, then modifications are applied + * + * @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo + * @param array $args + * @param \Illuminate\Database\Eloquent\Builder | \Laravel\Scout\Builder $query + * + * @return void + */ + public static function applyTrashedModificationIfNeeded(ResolveInfo $resolveInfo, array $args, $query): void { + // skip execution, if model doesn't support soft delete + if (!in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($query->getModel()))) { + return; + } + + $trashedArgumentName = null; + + // get field definition + $fieldDefinition = $resolveInfo->parentType->getField($resolveInfo->fieldName); + if (!$fieldDefinition instanceof FieldDefinition) { + return; + } + + // search for trashed argument name + foreach ($fieldDefinition->args as $fieldArgument) { + $fieldArgumentType = $fieldArgument->getType(); + if ($fieldArgumentType instanceof EnumType && $fieldArgumentType->name === 'Trash') { + $trashedArgumentName = $fieldArgument->name; + } + } + + // apply trashed query modification + if ($trashedArgumentName !== null && array_key_exists($trashedArgumentName, $args)) { + $trashModificationMethod = "{$args[$trashedArgumentName]}Trashed"; + $query->$trashModificationMethod(); + } + } } diff --git a/tests/Integration/Schema/Directives/AllDirectiveTest.php b/tests/Integration/Schema/Directives/AllDirectiveTest.php index f6a0f36a36..f7f6072b72 100644 --- a/tests/Integration/Schema/Directives/AllDirectiveTest.php +++ b/tests/Integration/Schema/Directives/AllDirectiveTest.php @@ -4,6 +4,7 @@ use Tests\DBTestCase; use Tests\Utils\Models\Post; +use Tests\Utils\Models\Task; use Tests\Utils\Models\User; class AllDirectiveTest extends DBTestCase @@ -124,4 +125,103 @@ public function itCanGetAllModelsFiltered(): void } ')->assertJsonCount(2, 'data.users'); } + + /** + * @test + */ + public function itCanApplyTrashedArgument(): void + { + $tasks = factory(Task::class, 3)->create(); + $taskToRemove = $tasks[2]; + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + tasks(trashed: Trash): [Task!]! @all + } + '; + + $this->graphQL(' + { + tasks(trashed: ONLY) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'tasks' => [ + [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ] + ] + ] + ]); + + $this->graphQL(' + { + tasks(trashed: WITH) { + id + name + } + } + ')->assertJsonCount(3, 'data.tasks'); + + $this->graphQL(' + { + tasks(trashed: WITHOUT) { + id + name + } + } + ')->assertJsonCount(2, 'data.tasks'); + } + + + /** + * @test + */ + public function itCanFetchWithoutTrashedOnMissedTrashedArgument(): void + { + $tasks = factory(Task::class, 3)->create(); + $taskToRemove = $tasks[2]; + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + tasks(trashed: Trash): [Task!]! @all + tasks2: [Task!]! @all + } + '; + + $this->graphQL(' + { + tasks { + id + name + } + } + ')->assertJsonCount(2, 'data.tasks'); + + $this->graphQL(' + { + tasks2 { + id + name + } + } + ')->assertJsonCount(2, 'data.tasks2'); + } + } diff --git a/tests/Integration/Schema/Directives/FindDirectiveTest.php b/tests/Integration/Schema/Directives/FindDirectiveTest.php index a15ce2e206..5aa6a4ab16 100644 --- a/tests/Integration/Schema/Directives/FindDirectiveTest.php +++ b/tests/Integration/Schema/Directives/FindDirectiveTest.php @@ -3,6 +3,7 @@ namespace Tests\Integration\Schema\Directives; use Tests\DBTestCase; +use Tests\Utils\Models\Task; use Tests\Utils\Models\User; use Tests\Utils\Models\Company; @@ -172,4 +173,117 @@ public function itReturnsAnEmptyObjectWhenTheModelIsNotFound(): void ], ])->assertStatus(200); } + + + /** + * @test + */ + public function itCanApplyTrashedArgument(): void + { + $taskToRemove = factory(Task::class)->create(); + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + task(id: ID! @eq, trashed: Trash): Task @find + } + '; + + $this->graphQL(' + { + task(id: 1, trashed: ONLY) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ] + ] + ]); + + $this->graphQL(' + { + task(id: 1, trashed: WITH) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ] + ] + ]); + + $this->graphQL(' + { + task(id: 1, trashed: WITHOUT) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => null + ] + ]); + } + + /** + * @test + */ + public function itCanReturnsNullOnMissedTrashedArgument(): void + { + $taskToRemove = factory(Task::class)->create(); + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + task(id: ID! @eq, trashed: Trash): Task @find + task2(id: ID! @eq): Task @find + } + '; + + $this->graphQL(' + { + task(id: 1) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => null + ] + ]); + + $this->graphQL(' + { + task2(id: 1) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task2' => null + ] + ]); + } } diff --git a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php index 166205f12c..01abed5853 100644 --- a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php +++ b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php @@ -5,6 +5,7 @@ use Tests\DBTestCase; use GraphQL\Error\Error; use Tests\Utils\Models\Post; +use Tests\Utils\Models\Task; use Tests\Utils\Models\User; use Tests\Utils\Models\Comment; use Illuminate\Database\Eloquent\Builder; @@ -552,4 +553,114 @@ public function itIsLimitedByMaxCountFromDirective(): void } ')->assertJsonCount(10, 'data.users2.data'); } + + /** + * @test + */ + public function itCanApplyTrashedArgument(): void + { + $tasks = factory(Task::class, 3)->create(); + $taskToRemove = $tasks[2]; + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + tasks(trashed: Trash): [Task!]! @paginate + } + '; + + $this->graphQL(' + { + tasks(first: 10, trashed: ONLY) { + data { + id + name + } + } + } + ')->assertJson([ + 'data' => [ + 'tasks' => [ + 'data' => [ + [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ] + ] + ] + ] + ]); + + $this->graphQL(' + { + tasks(first: 10, trashed: WITH) { + data { + id + name + } + } + } + ')->assertJsonCount(3, 'data.tasks.data'); + + $this->graphQL(' + { + tasks(first: 10, trashed: WITHOUT) { + data { + id + name + } + } + } + ')->assertJsonCount(2, 'data.tasks.data'); + } + + /** + * @test + */ + public function itCanFetchWithoutTrashedOnMissedTrashedArgument(): void + { + $tasks = factory(Task::class, 3)->create(); + $taskToRemove = $tasks[2]; + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + tasks(trashed: Trash): [Task!]! @paginate + tasks2: [Task!]! @paginate + } + '; + + $this->graphQL(' + { + tasks(first: 10) { + data { + id + name + } + } + } + ')->assertJsonCount(2, 'data.tasks.data'); + + $this->graphQL(' + { + tasks2(first: 10) { + data { + id + name + } + } + } + ')->assertJsonCount(2, 'data.tasks2.data'); + } + } From f549bd0e5d22f6a7002f97dacabb42fc5ec7aeb2 Mon Sep 17 00:00:00 2001 From: lorado Date: Tue, 27 Aug 2019 22:09:39 +0200 Subject: [PATCH 02/19] fix styleci --- src/Schema/Directives/AllDirective.php | 2 +- src/Schema/Directives/FindDirective.php | 2 +- src/Schema/Directives/PaginateDirective.php | 2 +- src/Support/Utils.php | 15 +++++++------- .../Schema/Directives/AllDirectiveTest.php | 6 +++--- .../Schema/Directives/FindDirectiveTest.php | 20 +++++++++---------- .../Directives/PaginateDirectiveTest.php | 8 ++++---- 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/Schema/Directives/AllDirective.php b/src/Schema/Directives/AllDirective.php index 4a9cdd99fe..e187a996d4 100644 --- a/src/Schema/Directives/AllDirective.php +++ b/src/Schema/Directives/AllDirective.php @@ -2,12 +2,12 @@ namespace Nuwave\Lighthouse\Schema\Directives; +use Nuwave\Lighthouse\Support\Utils; use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Database\Eloquent\Collection; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; -use Nuwave\Lighthouse\Support\Utils; class AllDirective extends BaseDirective implements FieldResolver { diff --git a/src/Schema/Directives/FindDirective.php b/src/Schema/Directives/FindDirective.php index 638de3a948..881284dd70 100644 --- a/src/Schema/Directives/FindDirective.php +++ b/src/Schema/Directives/FindDirective.php @@ -3,12 +3,12 @@ namespace Nuwave\Lighthouse\Schema\Directives; use GraphQL\Error\Error; +use Nuwave\Lighthouse\Support\Utils; use Illuminate\Database\Eloquent\Model; use GraphQL\Type\Definition\ResolveInfo; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; -use Nuwave\Lighthouse\Support\Utils; class FindDirective extends BaseDirective implements FieldResolver { diff --git a/src/Schema/Directives/PaginateDirective.php b/src/Schema/Directives/PaginateDirective.php index 3e7fad8546..35c5035a32 100644 --- a/src/Schema/Directives/PaginateDirective.php +++ b/src/Schema/Directives/PaginateDirective.php @@ -3,6 +3,7 @@ namespace Nuwave\Lighthouse\Schema\Directives; use Illuminate\Support\Str; +use Nuwave\Lighthouse\Support\Utils; use GraphQL\Type\Definition\ResolveInfo; use Laravel\Scout\Builder as ScoutBuilder; use Nuwave\Lighthouse\Schema\AST\ASTHelper; @@ -18,7 +19,6 @@ use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Nuwave\Lighthouse\Support\Contracts\FieldManipulator; -use Nuwave\Lighthouse\Support\Utils; class PaginateDirective extends BaseDirective implements FieldResolver, FieldManipulator { diff --git a/src/Support/Utils.php b/src/Support/Utils.php index b73b37af96..cd8e47251c 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -3,11 +3,11 @@ namespace Nuwave\Lighthouse\Support; use Closure; -use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\FieldDefinition; -use GraphQL\Type\Definition\ResolveInfo; use ReflectionClass; use ReflectionException; +use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\ResolveInfo; +use GraphQL\Type\Definition\FieldDefinition; use Nuwave\Lighthouse\Exceptions\DefinitionException; class Utils @@ -86,7 +86,7 @@ public static function accessProtected($object, string $memberName, $default = n /** * Apply withTrashed, onlyTrashed or withoutTrashed to given $query if needed. * Resolve info is used to get list if argument definitions of current field. - * If there is any argument of enum type Trash, then modifications are applied + * If there is any argument of enum type Trash, then modifications are applied. * * @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo * @param array $args @@ -94,9 +94,10 @@ public static function accessProtected($object, string $memberName, $default = n * * @return void */ - public static function applyTrashedModificationIfNeeded(ResolveInfo $resolveInfo, array $args, $query): void { + public static function applyTrashedModificationIfNeeded(ResolveInfo $resolveInfo, array $args, $query): void + { // skip execution, if model doesn't support soft delete - if (!in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($query->getModel()))) { + if (! in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($query->getModel()))) { return; } @@ -104,7 +105,7 @@ public static function applyTrashedModificationIfNeeded(ResolveInfo $resolveInfo // get field definition $fieldDefinition = $resolveInfo->parentType->getField($resolveInfo->fieldName); - if (!$fieldDefinition instanceof FieldDefinition) { + if (! $fieldDefinition instanceof FieldDefinition) { return; } diff --git a/tests/Integration/Schema/Directives/AllDirectiveTest.php b/tests/Integration/Schema/Directives/AllDirectiveTest.php index f7f6072b72..dc036a3dd0 100644 --- a/tests/Integration/Schema/Directives/AllDirectiveTest.php +++ b/tests/Integration/Schema/Directives/AllDirectiveTest.php @@ -159,9 +159,9 @@ public function itCanApplyTrashedArgument(): void [ 'id' => $taskToRemove->id, 'name' => $taskToRemove->name, - ] - ] - ] + ], + ], + ], ]); $this->graphQL(' diff --git a/tests/Integration/Schema/Directives/FindDirectiveTest.php b/tests/Integration/Schema/Directives/FindDirectiveTest.php index 5aa6a4ab16..4abf219ce1 100644 --- a/tests/Integration/Schema/Directives/FindDirectiveTest.php +++ b/tests/Integration/Schema/Directives/FindDirectiveTest.php @@ -206,8 +206,8 @@ public function itCanApplyTrashedArgument(): void 'task' => [ 'id' => $taskToRemove->id, 'name' => $taskToRemove->name, - ] - ] + ], + ], ]); $this->graphQL(' @@ -222,8 +222,8 @@ public function itCanApplyTrashedArgument(): void 'task' => [ 'id' => $taskToRemove->id, 'name' => $taskToRemove->name, - ] - ] + ], + ], ]); $this->graphQL(' @@ -235,8 +235,8 @@ public function itCanApplyTrashedArgument(): void } ')->assertJson([ 'data' => [ - 'task' => null - ] + 'task' => null, + ], ]); } @@ -269,8 +269,8 @@ public function itCanReturnsNullOnMissedTrashedArgument(): void } ')->assertJson([ 'data' => [ - 'task' => null - ] + 'task' => null, + ], ]); $this->graphQL(' @@ -282,8 +282,8 @@ public function itCanReturnsNullOnMissedTrashedArgument(): void } ')->assertJson([ 'data' => [ - 'task2' => null - ] + 'task2' => null, + ], ]); } } diff --git a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php index 01abed5853..eaf9f28b7e 100644 --- a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php +++ b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php @@ -590,10 +590,10 @@ public function itCanApplyTrashedArgument(): void [ 'id' => $taskToRemove->id, 'name' => $taskToRemove->name, - ] - ] - ] - ] + ], + ], + ], + ], ]); $this->graphQL(' From 98a7030b201ff75b557e12cdf6e520336d5ef00d Mon Sep 17 00:00:00 2001 From: lorado Date: Tue, 27 Aug 2019 22:32:00 +0200 Subject: [PATCH 03/19] fix retrieving model from scout builder --- src/Support/Utils.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Support/Utils.php b/src/Support/Utils.php index cd8e47251c..178d242421 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -3,6 +3,7 @@ namespace Nuwave\Lighthouse\Support; use Closure; +use Laravel\Scout\Builder as ScoutBuilder; use ReflectionClass; use ReflectionException; use GraphQL\Type\Definition\EnumType; @@ -97,7 +98,11 @@ public static function accessProtected($object, string $memberName, $default = n public static function applyTrashedModificationIfNeeded(ResolveInfo $resolveInfo, array $args, $query): void { // skip execution, if model doesn't support soft delete - if (! in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($query->getModel()))) { + $model = $query instanceof ScoutBuilder + ? $query->model + : $query->getModel(); + + if (! in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($model))) { return; } From 6ecdab6ffdade31df715bb3c436b81d639542f40 Mon Sep 17 00:00:00 2001 From: lorado Date: Tue, 27 Aug 2019 22:34:01 +0200 Subject: [PATCH 04/19] fix style --- src/Support/Utils.php | 2 +- tests/Integration/Schema/Directives/AllDirectiveTest.php | 2 -- tests/Integration/Schema/Directives/FindDirectiveTest.php | 1 - tests/Integration/Schema/Directives/PaginateDirectiveTest.php | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 178d242421..f37edeacc3 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -3,11 +3,11 @@ namespace Nuwave\Lighthouse\Support; use Closure; -use Laravel\Scout\Builder as ScoutBuilder; use ReflectionClass; use ReflectionException; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\ResolveInfo; +use Laravel\Scout\Builder as ScoutBuilder; use GraphQL\Type\Definition\FieldDefinition; use Nuwave\Lighthouse\Exceptions\DefinitionException; diff --git a/tests/Integration/Schema/Directives/AllDirectiveTest.php b/tests/Integration/Schema/Directives/AllDirectiveTest.php index dc036a3dd0..b03ad712b8 100644 --- a/tests/Integration/Schema/Directives/AllDirectiveTest.php +++ b/tests/Integration/Schema/Directives/AllDirectiveTest.php @@ -183,7 +183,6 @@ public function itCanApplyTrashedArgument(): void ')->assertJsonCount(2, 'data.tasks'); } - /** * @test */ @@ -223,5 +222,4 @@ public function itCanFetchWithoutTrashedOnMissedTrashedArgument(): void } ')->assertJsonCount(2, 'data.tasks2'); } - } diff --git a/tests/Integration/Schema/Directives/FindDirectiveTest.php b/tests/Integration/Schema/Directives/FindDirectiveTest.php index 4abf219ce1..ab3bb4a5e7 100644 --- a/tests/Integration/Schema/Directives/FindDirectiveTest.php +++ b/tests/Integration/Schema/Directives/FindDirectiveTest.php @@ -174,7 +174,6 @@ public function itReturnsAnEmptyObjectWhenTheModelIsNotFound(): void ])->assertStatus(200); } - /** * @test */ diff --git a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php index eaf9f28b7e..d0db4bd0eb 100644 --- a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php +++ b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php @@ -662,5 +662,4 @@ public function itCanFetchWithoutTrashedOnMissedTrashedArgument(): void } ')->assertJsonCount(2, 'data.tasks2.data'); } - } From c2eab23549370d1d8fa3d68f9ced09f16f14c1a9 Mon Sep 17 00:00:00 2001 From: lorado Date: Thu, 29 Aug 2019 16:58:16 +0200 Subject: [PATCH 05/19] Rename chapter to match laravel docs chapter --- docs/master/eloquent/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/master/eloquent/getting-started.md b/docs/master/eloquent/getting-started.md index 867af60e5c..d304aeffe3 100644 --- a/docs/master/eloquent/getting-started.md +++ b/docs/master/eloquent/getting-started.md @@ -250,7 +250,7 @@ This mutation will return the deleted object, so you will have a last chance to } ``` -## Soft delete +## Soft Deleting If your model uses soft delete, you can define an attribute with enum type `Trash`, that is provided by lighthouse. It has `ONLY`, `WITH` and `WITHOUT` values, according laravels `onlyTrashed()`, `withTrashed()` and `withoutTrashed()` methods. From 4d81f76dfb6e1276b2c6705e5a35f34962be8f33 Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 02:08:13 +0200 Subject: [PATCH 06/19] refactor: implement @softDeletes and @trash directives --- src/Schema/Directives/AllDirective.php | 9 +-- src/Schema/Directives/FindDirective.php | 9 +-- src/Schema/Directives/PaginateDirective.php | 2 - .../Directives/SoftDeletesDirective.php | 36 ++++++++++++ src/Schema/Directives/TrashDirective.php | 58 +++++++++++++++++++ src/Support/Utils.php | 45 -------------- 6 files changed, 100 insertions(+), 59 deletions(-) create mode 100644 src/Schema/Directives/SoftDeletesDirective.php create mode 100644 src/Schema/Directives/TrashDirective.php diff --git a/src/Schema/Directives/AllDirective.php b/src/Schema/Directives/AllDirective.php index e187a996d4..17ff672f9f 100644 --- a/src/Schema/Directives/AllDirective.php +++ b/src/Schema/Directives/AllDirective.php @@ -34,7 +34,7 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) /** @var \Illuminate\Database\Eloquent\Model $modelClass */ $modelClass = $this->getModelClass(); - $query = $resolveInfo + return $resolveInfo ->builder ->addScopes( $this->directiveArgValue('scopes', []) @@ -42,11 +42,8 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) ->apply( $modelClass::query(), $args - ); - - Utils::applyTrashedModificationIfNeeded($resolveInfo, $args, $query); - - return $query->get(); + ) + ->get(); } ); } diff --git a/src/Schema/Directives/FindDirective.php b/src/Schema/Directives/FindDirective.php index 881284dd70..432e0d2ee7 100644 --- a/src/Schema/Directives/FindDirective.php +++ b/src/Schema/Directives/FindDirective.php @@ -35,7 +35,7 @@ public function resolveField(FieldValue $fieldValue): FieldValue return $fieldValue->setResolver( function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($model): ?Model { - $query = $resolveInfo + $results = $resolveInfo ->builder ->addScopes( $this->directiveArgValue('scopes', []) @@ -43,11 +43,8 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) ->apply( $model::query(), $args - ); - - Utils::applyTrashedModificationIfNeeded($resolveInfo, $args, $query); - - $results = $query->get(); + ) + ->get(); if ($results->count() > 1) { throw new Error('The query returned more than one result.'); diff --git a/src/Schema/Directives/PaginateDirective.php b/src/Schema/Directives/PaginateDirective.php index 35c5035a32..2b81cf6b38 100644 --- a/src/Schema/Directives/PaginateDirective.php +++ b/src/Schema/Directives/PaginateDirective.php @@ -88,8 +88,6 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) $args ); - Utils::applyTrashedModificationIfNeeded($resolveInfo, $args, $query); - if ($query instanceof ScoutBuilder) { return $query->paginate($first, 'page', $page); } diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/Schema/Directives/SoftDeletesDirective.php new file mode 100644 index 0000000000..3265b06413 --- /dev/null +++ b/src/Schema/Directives/SoftDeletesDirective.php @@ -0,0 +1,36 @@ +arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, [$softDeletesArgument]); + } + +} diff --git a/src/Schema/Directives/TrashDirective.php b/src/Schema/Directives/TrashDirective.php new file mode 100644 index 0000000000..81c215d60f --- /dev/null +++ b/src/Schema/Directives/TrashDirective.php @@ -0,0 +1,58 @@ +getRelated(); + $query = $builder->getQuery(); + } else { + $model = $builder instanceof ScoutBuilder + ? $builder->model + : $builder->getModel(); + $query = $builder; + } + + if (! in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($model))) { + return $builder; + } + + // apply trashed query modification + if (!isset($value)) { + return $builder; + } + + $trashModificationMethod = "{$value}Trashed"; + $query->{$trashModificationMethod}(); + + return $builder; + } +} diff --git a/src/Support/Utils.php b/src/Support/Utils.php index f37edeacc3..ca2bb69aae 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -83,49 +83,4 @@ public static function accessProtected($object, string $memberName, $default = n return $default; } } - - /** - * Apply withTrashed, onlyTrashed or withoutTrashed to given $query if needed. - * Resolve info is used to get list if argument definitions of current field. - * If there is any argument of enum type Trash, then modifications are applied. - * - * @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo - * @param array $args - * @param \Illuminate\Database\Eloquent\Builder | \Laravel\Scout\Builder $query - * - * @return void - */ - public static function applyTrashedModificationIfNeeded(ResolveInfo $resolveInfo, array $args, $query): void - { - // skip execution, if model doesn't support soft delete - $model = $query instanceof ScoutBuilder - ? $query->model - : $query->getModel(); - - if (! in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($model))) { - return; - } - - $trashedArgumentName = null; - - // get field definition - $fieldDefinition = $resolveInfo->parentType->getField($resolveInfo->fieldName); - if (! $fieldDefinition instanceof FieldDefinition) { - return; - } - - // search for trashed argument name - foreach ($fieldDefinition->args as $fieldArgument) { - $fieldArgumentType = $fieldArgument->getType(); - if ($fieldArgumentType instanceof EnumType && $fieldArgumentType->name === 'Trash') { - $trashedArgumentName = $fieldArgument->name; - } - } - - // apply trashed query modification - if ($trashedArgumentName !== null && array_key_exists($trashedArgumentName, $args)) { - $trashModificationMethod = "{$args[$trashedArgumentName]}Trashed"; - $query->$trashModificationMethod(); - } - } } From 21fe7883eff9a3224956b5c1cfcf497e28875c9c Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 02:08:30 +0200 Subject: [PATCH 07/19] update tests --- .../Schema/Directives/AllDirectiveTest.php | 97 ----- .../Schema/Directives/FindDirectiveTest.php | 112 ----- .../Directives/PaginateDirectiveTest.php | 109 ----- .../SoftDeletesAndTrashDirectiveTest.php | 392 ++++++++++++++++++ 4 files changed, 392 insertions(+), 318 deletions(-) create mode 100644 tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php diff --git a/tests/Integration/Schema/Directives/AllDirectiveTest.php b/tests/Integration/Schema/Directives/AllDirectiveTest.php index b03ad712b8..5b7e2fd05a 100644 --- a/tests/Integration/Schema/Directives/AllDirectiveTest.php +++ b/tests/Integration/Schema/Directives/AllDirectiveTest.php @@ -125,101 +125,4 @@ public function itCanGetAllModelsFiltered(): void } ')->assertJsonCount(2, 'data.users'); } - - /** - * @test - */ - public function itCanApplyTrashedArgument(): void - { - $tasks = factory(Task::class, 3)->create(); - $taskToRemove = $tasks[2]; - $taskToRemove->delete(); - - $this->schema = ' - type Task { - id: ID! - name: String! - } - - type Query { - tasks(trashed: Trash): [Task!]! @all - } - '; - - $this->graphQL(' - { - tasks(trashed: ONLY) { - id - name - } - } - ')->assertJson([ - 'data' => [ - 'tasks' => [ - [ - 'id' => $taskToRemove->id, - 'name' => $taskToRemove->name, - ], - ], - ], - ]); - - $this->graphQL(' - { - tasks(trashed: WITH) { - id - name - } - } - ')->assertJsonCount(3, 'data.tasks'); - - $this->graphQL(' - { - tasks(trashed: WITHOUT) { - id - name - } - } - ')->assertJsonCount(2, 'data.tasks'); - } - - /** - * @test - */ - public function itCanFetchWithoutTrashedOnMissedTrashedArgument(): void - { - $tasks = factory(Task::class, 3)->create(); - $taskToRemove = $tasks[2]; - $taskToRemove->delete(); - - $this->schema = ' - type Task { - id: ID! - name: String! - } - - type Query { - tasks(trashed: Trash): [Task!]! @all - tasks2: [Task!]! @all - } - '; - - $this->graphQL(' - { - tasks { - id - name - } - } - ')->assertJsonCount(2, 'data.tasks'); - - $this->graphQL(' - { - tasks2 { - id - name - } - } - ')->assertJsonCount(2, 'data.tasks2'); - } } diff --git a/tests/Integration/Schema/Directives/FindDirectiveTest.php b/tests/Integration/Schema/Directives/FindDirectiveTest.php index ab3bb4a5e7..eb1801314a 100644 --- a/tests/Integration/Schema/Directives/FindDirectiveTest.php +++ b/tests/Integration/Schema/Directives/FindDirectiveTest.php @@ -173,116 +173,4 @@ public function itReturnsAnEmptyObjectWhenTheModelIsNotFound(): void ], ])->assertStatus(200); } - - /** - * @test - */ - public function itCanApplyTrashedArgument(): void - { - $taskToRemove = factory(Task::class)->create(); - $taskToRemove->delete(); - - $this->schema = ' - type Task { - id: ID! - name: String! - } - - type Query { - task(id: ID! @eq, trashed: Trash): Task @find - } - '; - - $this->graphQL(' - { - task(id: 1, trashed: ONLY) { - id - name - } - } - ')->assertJson([ - 'data' => [ - 'task' => [ - 'id' => $taskToRemove->id, - 'name' => $taskToRemove->name, - ], - ], - ]); - - $this->graphQL(' - { - task(id: 1, trashed: WITH) { - id - name - } - } - ')->assertJson([ - 'data' => [ - 'task' => [ - 'id' => $taskToRemove->id, - 'name' => $taskToRemove->name, - ], - ], - ]); - - $this->graphQL(' - { - task(id: 1, trashed: WITHOUT) { - id - name - } - } - ')->assertJson([ - 'data' => [ - 'task' => null, - ], - ]); - } - - /** - * @test - */ - public function itCanReturnsNullOnMissedTrashedArgument(): void - { - $taskToRemove = factory(Task::class)->create(); - $taskToRemove->delete(); - - $this->schema = ' - type Task { - id: ID! - name: String! - } - - type Query { - task(id: ID! @eq, trashed: Trash): Task @find - task2(id: ID! @eq): Task @find - } - '; - - $this->graphQL(' - { - task(id: 1) { - id - name - } - } - ')->assertJson([ - 'data' => [ - 'task' => null, - ], - ]); - - $this->graphQL(' - { - task2(id: 1) { - id - name - } - } - ')->assertJson([ - 'data' => [ - 'task2' => null, - ], - ]); - } } diff --git a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php index d0db4bd0eb..9e36d90363 100644 --- a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php +++ b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php @@ -553,113 +553,4 @@ public function itIsLimitedByMaxCountFromDirective(): void } ')->assertJsonCount(10, 'data.users2.data'); } - - /** - * @test - */ - public function itCanApplyTrashedArgument(): void - { - $tasks = factory(Task::class, 3)->create(); - $taskToRemove = $tasks[2]; - $taskToRemove->delete(); - - $this->schema = ' - type Task { - id: ID! - name: String! - } - - type Query { - tasks(trashed: Trash): [Task!]! @paginate - } - '; - - $this->graphQL(' - { - tasks(first: 10, trashed: ONLY) { - data { - id - name - } - } - } - ')->assertJson([ - 'data' => [ - 'tasks' => [ - 'data' => [ - [ - 'id' => $taskToRemove->id, - 'name' => $taskToRemove->name, - ], - ], - ], - ], - ]); - - $this->graphQL(' - { - tasks(first: 10, trashed: WITH) { - data { - id - name - } - } - } - ')->assertJsonCount(3, 'data.tasks.data'); - - $this->graphQL(' - { - tasks(first: 10, trashed: WITHOUT) { - data { - id - name - } - } - } - ')->assertJsonCount(2, 'data.tasks.data'); - } - - /** - * @test - */ - public function itCanFetchWithoutTrashedOnMissedTrashedArgument(): void - { - $tasks = factory(Task::class, 3)->create(); - $taskToRemove = $tasks[2]; - $taskToRemove->delete(); - - $this->schema = ' - type Task { - id: ID! - name: String! - } - - type Query { - tasks(trashed: Trash): [Task!]! @paginate - tasks2: [Task!]! @paginate - } - '; - - $this->graphQL(' - { - tasks(first: 10) { - data { - id - name - } - } - } - ')->assertJsonCount(2, 'data.tasks.data'); - - $this->graphQL(' - { - tasks2(first: 10) { - data { - id - name - } - } - } - ')->assertJsonCount(2, 'data.tasks2.data'); - } } diff --git a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php new file mode 100644 index 0000000000..2eb4591fee --- /dev/null +++ b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php @@ -0,0 +1,392 @@ +create(); + $taskToRemove = $tasks[2]; + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + tasks: [Task!]! @all @softDeletes + } + '; + + $this->graphQL(' + { + tasks(trashed: ONLY) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'tasks' => [ + [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ], + ], + ], + ]); + + $this->graphQL(' + { + tasks(trashed: WITH) { + id + name + } + } + ')->assertJsonCount(3, 'data.tasks'); + + $this->graphQL(' + { + tasks(trashed: WITHOUT) { + id + name + } + } + ')->assertJsonCount(2, 'data.tasks'); + + $this->graphQL(' + { + tasks { + id + name + } + } + ')->assertJsonCount(2, 'data.tasks'); + } + + /** + * @test + */ + public function itCanBeUsedWithFindDirective(): void + { + $taskToRemove = factory(Task::class)->create(); + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + task(id: ID! @eq): Task @find @softDeletes + } + '; + + $this->graphQL(' + { + task(id: 1, trashed: ONLY) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ], + ], + ]); + + $this->graphQL(' + { + task(id: 1, trashed: WITH) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ], + ], + ]); + + $this->graphQL(' + { + task(id: 1, trashed: WITHOUT) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => null, + ], + ]); + + $this->graphQL(' + { + task(id: 1) { + id + name + } + } + ')->assertJson([ + 'data' => [ + 'task' => null, + ], + ]); + } + + /** + * @test + */ + public function itCanCanBeUsedWIthPaginateDirective(): void + { + $tasks = factory(Task::class, 3)->create(); + $taskToRemove = $tasks[2]; + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type Query { + tasks: [Task!]! @paginate @softDeletes + } + '; + + $this->graphQL(' + { + tasks(first: 10, trashed: ONLY) { + data { + id + name + } + } + } + ')->assertJson([ + 'data' => [ + 'tasks' => [ + 'data' => [ + [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ], + ], + ], + ], + ]); + + $this->graphQL(' + { + tasks(first: 10, trashed: WITH) { + data { + id + name + } + } + } + ')->assertJsonCount(3, 'data.tasks.data'); + + $this->graphQL(' + { + tasks(first: 10, trashed: WITHOUT) { + data { + id + name + } + } + } + ')->assertJsonCount(2, 'data.tasks.data'); + + $this->graphQL(' + { + tasks(first: 10) { + data { + id + name + } + } + } + ')->assertJsonCount(2, 'data.tasks.data'); + } + + /** + * @test + */ + public function itCanBeUsedNested(): void + { + $taskToRemove = factory(Task::class)->create(); + factory(Task::class, 2)->create(['user_id' => $taskToRemove->user->id]); + $taskToRemove->delete(); + + $this->schema = ' + type Task { + id: ID! + name: String! + } + + type User { + id: ID! + tasks: [Task!]! @hasMany @softDeletes + } + + type Query { + users: [User!]! @all + usersPaginated: [User!]! @paginate + user(id: ID! @eq): User @find + } + '; + + $this->graphQL(' + { + users { + tasks(trashed: ONLY) { + id + name + } + } + } + ')->assertJson([ + 'data' => [ + 'users' => [ + [ + 'tasks' => [ + [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ], + ], + ], + ], + ], + ]); + + $this->graphQL(' + { + usersPaginated(first: 10) { + data { + tasks(trashed: ONLY) { + id + name + } + } + } + } + ')->assertJson([ + 'data' => [ + 'usersPaginated' => [ + 'data' => [ + [ + 'tasks' => [ + [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ], + ], + ], + ], + ], + ], + ]); + + $this->graphQL(' + { + user(id: 1) { + tasks(trashed: ONLY) { + id + name + } + } + } + ')->assertJson([ + 'data' => [ + 'user' => [ + 'tasks' => [ + [ + 'id' => $taskToRemove->id, + 'name' => $taskToRemove->name, + ], + ], + ], + ], + ]); + + $this->graphQL(' + { + users { + tasksWith: tasks(trashed: WITH) { + id + } + tasksWithout: tasks(trashed: WITHOUT) { + id + } + tasksSimple: tasks { + id + } + } + } + ') + ->assertJsonCount(3, 'data.users.0.tasksWith') + ->assertJsonCount(2, 'data.users.0.tasksWithout') + ->assertJsonCount(2, 'data.users.0.tasksSimple'); + + $this->graphQL(' + { + usersPaginated(first: 10) { + data { + tasksWith: tasks(trashed: WITH) { + id + } + tasksWithout: tasks(trashed: WITHOUT) { + id + } + tasksSimple: tasks { + id + } + } + } + } + ') + ->assertJsonCount(3, 'data.usersPaginated.data.0.tasksWith') + ->assertJsonCount(2, 'data.usersPaginated.data.0.tasksWithout') + ->assertJsonCount(2, 'data.usersPaginated.data.0.tasksSimple'); + + $this->graphQL(' + { + user(id: 1) { + tasksWith: tasks(trashed: WITH) { + id + } + tasksWithout: tasks(trashed: WITHOUT) { + id + } + tasksSimple: tasks { + id + } + } + } + ') + ->assertJsonCount(3, 'data.user.tasksWith') + ->assertJsonCount(2, 'data.user.tasksWithout') + ->assertJsonCount(2, 'data.user.tasksSimple'); + } +} From f7e22c51e65e295000f637cf050af06a3b49c35a Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 02:10:03 +0200 Subject: [PATCH 08/19] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 630ab757be..cb990410c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add support for defining `onlyTrashed`, `withTrashed`, and `withoutTrashed` on models with soft delete for `@all`, `@find` and `@paginate` directives +- Add `@softDeletes` and `@trash` directives to be able to fetch `onlyTrashed`, `withTrashed` or `withoutTrashed` models https://github.com/nuwave/lighthouse/pull/937 - Add `@morphTo` directive for polymorphic one-to-one relationships https://github.com/nuwave/lighthouse/pull/921 - Support Laravel `^6.0` https://github.com/nuwave/lighthouse/pull/926 From 7d3a6f396988d325e42636401fdb1a767a2a77d9 Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 02:14:52 +0200 Subject: [PATCH 09/19] update readme --- docs/master/eloquent/getting-started.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/master/eloquent/getting-started.md b/docs/master/eloquent/getting-started.md index d304aeffe3..dc97a21cc3 100644 --- a/docs/master/eloquent/getting-started.md +++ b/docs/master/eloquent/getting-started.md @@ -252,17 +252,17 @@ This mutation will return the deleted object, so you will have a last chance to ## Soft Deleting -If your model uses soft delete, you can define an attribute with enum type `Trash`, that is provided by lighthouse. It has `ONLY`, `WITH` and `WITHOUT` values, according laravels `onlyTrashed()`, `withTrashed()` and `withoutTrashed()` methods. +If your model uses soft delete, you can use `@softDeletes` directive on your field, to be able to query `onlyTrashed`, `withTrashed` or `withoutTrashed` elements. -You are free to choose an attribute name you like, as lightouse searches for attributes of type `Trash` automatically. +`@softDeletes` directive adds new argument `trashed` of enum type `Trash` with available values `ONLY`, `WITH` and `WITHOUT` to your field. -Currently `@all`, `@paginate` and `@find` directives supports this feature +NOTE: You have to use this directive aside builtin field directives like `@all`, `@paginate`, `@find`, `@hasMany`, `@hasOne` etc. For example your schema has following structure ```graphql type Query { - tasks(trashed: Trash): [Task!]! @all + tasks: [Task!]! @all @softDeletes } ``` From 9b47fa2083022c871945aa58d6d8886b82f6621f Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 02:31:47 +0200 Subject: [PATCH 10/19] fix code style --- src/Schema/Directives/AllDirective.php | 1 - src/Schema/Directives/FindDirective.php | 1 - src/Schema/Directives/PaginateDirective.php | 1 - src/Schema/Directives/SoftDeletesDirective.php | 4 ++-- src/Schema/Directives/TrashDirective.php | 5 ++--- src/Support/Utils.php | 4 ---- tests/Integration/Schema/Directives/AllDirectiveTest.php | 1 - tests/Integration/Schema/Directives/FindDirectiveTest.php | 1 - .../Integration/Schema/Directives/PaginateDirectiveTest.php | 1 - .../Schema/Directives/SoftDeletesAndTrashDirectiveTest.php | 4 +--- 10 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/Schema/Directives/AllDirective.php b/src/Schema/Directives/AllDirective.php index 17ff672f9f..d43034e71f 100644 --- a/src/Schema/Directives/AllDirective.php +++ b/src/Schema/Directives/AllDirective.php @@ -2,7 +2,6 @@ namespace Nuwave\Lighthouse\Schema\Directives; -use Nuwave\Lighthouse\Support\Utils; use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Database\Eloquent\Collection; use Nuwave\Lighthouse\Schema\Values\FieldValue; diff --git a/src/Schema/Directives/FindDirective.php b/src/Schema/Directives/FindDirective.php index 432e0d2ee7..2303815fa7 100644 --- a/src/Schema/Directives/FindDirective.php +++ b/src/Schema/Directives/FindDirective.php @@ -3,7 +3,6 @@ namespace Nuwave\Lighthouse\Schema\Directives; use GraphQL\Error\Error; -use Nuwave\Lighthouse\Support\Utils; use Illuminate\Database\Eloquent\Model; use GraphQL\Type\Definition\ResolveInfo; use Nuwave\Lighthouse\Schema\Values\FieldValue; diff --git a/src/Schema/Directives/PaginateDirective.php b/src/Schema/Directives/PaginateDirective.php index 2b81cf6b38..4f6f0ac78c 100644 --- a/src/Schema/Directives/PaginateDirective.php +++ b/src/Schema/Directives/PaginateDirective.php @@ -3,7 +3,6 @@ namespace Nuwave\Lighthouse\Schema\Directives; use Illuminate\Support\Str; -use Nuwave\Lighthouse\Support\Utils; use GraphQL\Type\Definition\ResolveInfo; use Laravel\Scout\Builder as ScoutBuilder; use Nuwave\Lighthouse\Schema\AST\ASTHelper; diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/Schema/Directives/SoftDeletesDirective.php index 3265b06413..8a499455d7 100644 --- a/src/Schema/Directives/SoftDeletesDirective.php +++ b/src/Schema/Directives/SoftDeletesDirective.php @@ -2,11 +2,11 @@ namespace Nuwave\Lighthouse\Schema\Directives; -use GraphQL\Language\AST\FieldDefinitionNode; -use GraphQL\Language\AST\ObjectTypeDefinitionNode; use Nuwave\Lighthouse\Schema\AST\ASTHelper; +use GraphQL\Language\AST\FieldDefinitionNode; use Nuwave\Lighthouse\Schema\AST\DocumentAST; use Nuwave\Lighthouse\Schema\AST\PartialParser; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; use Nuwave\Lighthouse\Support\Contracts\FieldManipulator; class SoftDeletesDirective extends BaseDirective implements FieldManipulator diff --git a/src/Schema/Directives/TrashDirective.php b/src/Schema/Directives/TrashDirective.php index 81c215d60f..085c256b2b 100644 --- a/src/Schema/Directives/TrashDirective.php +++ b/src/Schema/Directives/TrashDirective.php @@ -2,9 +2,8 @@ namespace Nuwave\Lighthouse\Schema\Directives; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\Relation; use Laravel\Scout\Builder as ScoutBuilder; +use Illuminate\Database\Eloquent\Relations\Relation; use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective; class TrashDirective extends BaseDirective implements ArgBuilderDirective @@ -46,7 +45,7 @@ public function handleBuilder($builder, $value) } // apply trashed query modification - if (!isset($value)) { + if (! isset($value)) { return $builder; } diff --git a/src/Support/Utils.php b/src/Support/Utils.php index ca2bb69aae..d15c004466 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -5,10 +5,6 @@ use Closure; use ReflectionClass; use ReflectionException; -use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\ResolveInfo; -use Laravel\Scout\Builder as ScoutBuilder; -use GraphQL\Type\Definition\FieldDefinition; use Nuwave\Lighthouse\Exceptions\DefinitionException; class Utils diff --git a/tests/Integration/Schema/Directives/AllDirectiveTest.php b/tests/Integration/Schema/Directives/AllDirectiveTest.php index 5b7e2fd05a..f6a0f36a36 100644 --- a/tests/Integration/Schema/Directives/AllDirectiveTest.php +++ b/tests/Integration/Schema/Directives/AllDirectiveTest.php @@ -4,7 +4,6 @@ use Tests\DBTestCase; use Tests\Utils\Models\Post; -use Tests\Utils\Models\Task; use Tests\Utils\Models\User; class AllDirectiveTest extends DBTestCase diff --git a/tests/Integration/Schema/Directives/FindDirectiveTest.php b/tests/Integration/Schema/Directives/FindDirectiveTest.php index eb1801314a..a15ce2e206 100644 --- a/tests/Integration/Schema/Directives/FindDirectiveTest.php +++ b/tests/Integration/Schema/Directives/FindDirectiveTest.php @@ -3,7 +3,6 @@ namespace Tests\Integration\Schema\Directives; use Tests\DBTestCase; -use Tests\Utils\Models\Task; use Tests\Utils\Models\User; use Tests\Utils\Models\Company; diff --git a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php index 9e36d90363..166205f12c 100644 --- a/tests/Integration/Schema/Directives/PaginateDirectiveTest.php +++ b/tests/Integration/Schema/Directives/PaginateDirectiveTest.php @@ -5,7 +5,6 @@ use Tests\DBTestCase; use GraphQL\Error\Error; use Tests\Utils\Models\Post; -use Tests\Utils\Models\Task; use Tests\Utils\Models\User; use Tests\Utils\Models\Comment; use Illuminate\Database\Eloquent\Builder; diff --git a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php index 2eb4591fee..1435912ded 100644 --- a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php +++ b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php @@ -3,9 +3,7 @@ namespace Tests\Integration\Schema\Directives; use Tests\DBTestCase; -use Tests\Utils\Models\Post; use Tests\Utils\Models\Task; -use Tests\Utils\Models\User; class SoftDeletesAndTrashDirectiveTest extends DBTestCase { @@ -14,7 +12,7 @@ class SoftDeletesAndTrashDirectiveTest extends DBTestCase */ public function itCanBeUsedWithAllDirective(): void { - $tasks = factory(Task::class, 3)->create(); + $tasks = factory(Task::class, 3)->create(); $taskToRemove = $tasks[2]; $taskToRemove->delete(); From a3d2bac96cc949c9c2d1d3f4833f5283d1074e67 Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 02:32:55 +0200 Subject: [PATCH 11/19] fix code style --- src/Schema/Directives/SoftDeletesDirective.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/Schema/Directives/SoftDeletesDirective.php index 8a499455d7..57cf8922fe 100644 --- a/src/Schema/Directives/SoftDeletesDirective.php +++ b/src/Schema/Directives/SoftDeletesDirective.php @@ -32,5 +32,4 @@ public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefini $softDeletesArgument = PartialParser::inputValueDefinition("\"Define if soft deleted models should be also fetched.\"\ntrashed: Trash @trash"); $fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, [$softDeletesArgument]); } - } From eeb2276cb1aee6d93810f0d16cfe8baf71d693ff Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 02:45:42 +0200 Subject: [PATCH 12/19] add API docs for new directives --- docs/master/api-reference/directives.md | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index a65020616d..085b29d854 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -1918,6 +1918,34 @@ query myQuery($someTest: Boolean) { } ``` +## @softDeletes + +This directive adds an `trashed: Trash @trash` argument to you field, which allows you to define if `ONLY`, `WITH` +or `WITHOUT` trashed elements should be fetched. + +### Definition +```graphql +directive @softDeletes on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +``` + +### Examples + +With following schema + +```graphql +type Query { + tasks: [Tasks!]! @all @softDeletes +} +``` + +It is possible to create queries like this: + +```graphql +{ + tasks(trashed: ONLY) {...} +} +``` + ## @spread Spread out the nested values of an argument of type input object into it's parent. @@ -2005,6 +2033,16 @@ directive @subscription( ) on FIELD_DEFINITION ``` +## @trash + +Updates builder query to fetch only specified soft deleted elements. +This directive is used by [@softDeletes directive](#softdeletes) internally. + +### Definition +```graphql +directive @trash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +``` + ## @trim Run the `trim` function on an input value. From af5284618365fc566ed8ee5e94339fa813754e5e Mon Sep 17 00:00:00 2001 From: lorado Date: Fri, 30 Aug 2019 11:10:53 +0200 Subject: [PATCH 13/19] Merge from master, set directives definitions, update docs --- docs/master/api-reference/directives.md | 2 +- src/Schema/Directives/SoftDeletesDirective.php | 10 +++++++++- src/Schema/Directives/TrashDirective.php | 10 +++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index b49190e70c..cf73402e44 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -1999,7 +1999,7 @@ or `WITHOUT` trashed elements should be fetched. ### Definition ```graphql -directive @softDeletes on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @softDeletes on FIELD_DEFINITION ``` ### Examples diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/Schema/Directives/SoftDeletesDirective.php index 57cf8922fe..1ccd62d2a4 100644 --- a/src/Schema/Directives/SoftDeletesDirective.php +++ b/src/Schema/Directives/SoftDeletesDirective.php @@ -7,9 +7,10 @@ use Nuwave\Lighthouse\Schema\AST\DocumentAST; use Nuwave\Lighthouse\Schema\AST\PartialParser; use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; use Nuwave\Lighthouse\Support\Contracts\FieldManipulator; -class SoftDeletesDirective extends BaseDirective implements FieldManipulator +class SoftDeletesDirective extends BaseDirective implements FieldManipulator, DefinedDirective { /** * Name of the directive. @@ -21,6 +22,13 @@ public function name(): string return 'softDeletes'; } + public static function definition(): string + { + return /* @lang GraphQL */ <<<'SDL' +directive @softDeletes on FIELD_DEFINITION +SDL; + } + /** * @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST * @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition diff --git a/src/Schema/Directives/TrashDirective.php b/src/Schema/Directives/TrashDirective.php index 085c256b2b..79bbeb871e 100644 --- a/src/Schema/Directives/TrashDirective.php +++ b/src/Schema/Directives/TrashDirective.php @@ -4,9 +4,10 @@ use Laravel\Scout\Builder as ScoutBuilder; use Illuminate\Database\Eloquent\Relations\Relation; +use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective; -class TrashDirective extends BaseDirective implements ArgBuilderDirective +class TrashDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective { /** * Name of the directive. @@ -18,6 +19,13 @@ public function name(): string return 'trash'; } + public static function definition(): string + { + return /* @lang GraphQL */ <<<'SDL' +directive @trash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +SDL; + } + /** * Apply withTrashed, onlyTrashed or withoutTrashed to given $builder if needed. * If builder model doesn't support soft deletes, this argument will be ignored! From e125b2999b06ea9078d33ed7bdee105f68de1d72 Mon Sep 17 00:00:00 2001 From: spawnia Date: Mon, 2 Sep 2019 20:28:05 +0200 Subject: [PATCH 14/19] Move docs to seperate page --- docs/master/eloquent/getting-started.md | 28 +------------------- docs/master/eloquent/soft-deleting.md | 35 +++++++++++++++++++++++++ docs/master/sidebar.js | 1 + 3 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 docs/master/eloquent/soft-deleting.md diff --git a/docs/master/eloquent/getting-started.md b/docs/master/eloquent/getting-started.md index dc97a21cc3..c378c9a35f 100644 --- a/docs/master/eloquent/getting-started.md +++ b/docs/master/eloquent/getting-started.md @@ -94,7 +94,7 @@ And can be queried like this: } ``` -## Adding query constraints +## Adding Query Constraints Lighthouse provides built-in directives to enhance your queries by giving additional query capabilities to the client. @@ -249,29 +249,3 @@ This mutation will return the deleted object, so you will have a last chance to } } ``` - -## Soft Deleting - -If your model uses soft delete, you can use `@softDeletes` directive on your field, to be able to query `onlyTrashed`, `withTrashed` or `withoutTrashed` elements. - -`@softDeletes` directive adds new argument `trashed` of enum type `Trash` with available values `ONLY`, `WITH` and `WITHOUT` to your field. - -NOTE: You have to use this directive aside builtin field directives like `@all`, `@paginate`, `@find`, `@hasMany`, `@hasOne` etc. - -For example your schema has following structure - -```graphql -type Query { - tasks: [Task!]! @all @softDeletes -} -``` - -Then you can make queries like this: - -```graphql -{ - tasks(trashed: WITH) { - ... - } -} -``` diff --git a/docs/master/eloquent/soft-deleting.md b/docs/master/eloquent/soft-deleting.md new file mode 100644 index 0000000000..4cad04e9ef --- /dev/null +++ b/docs/master/eloquent/soft-deleting.md @@ -0,0 +1,35 @@ +# Soft Deleting + +Lighthouse offers convenient helpers to work with models that utilize +[soft deletes](https://laravel.com/docs/eloquent#soft-deleting). + +## Filter Soft Deleted Models + +If your model uses the `Illuminate\Database\Eloquent\SoftDeletes` trait, +you can add the [`@softDeletes`](../api-reference/directives.md) directive to a field +to be able to query `onlyTrashed`, `withTrashed` or `withoutTrashed` elements. + +```graphql +type Query { + flights: [Flight!]! @all @softDeletes +} +``` + +Lighthouse will add an argument `trashed` to the field definition +and automatically include the enum `Trash`. + +```graphql +type Query { + flights(trashed: Trash @trashed): [Flight!]! @all +} +``` + +You can include soft deleted models in your result with a query like this: + +```graphql +{ + flights(trashed: WITH) { + id + } +} +``` diff --git a/docs/master/sidebar.js b/docs/master/sidebar.js index 1cdf633953..fb38e286f4 100644 --- a/docs/master/sidebar.js +++ b/docs/master/sidebar.js @@ -22,6 +22,7 @@ module.exports = [ children: [ ['eloquent/getting-started', 'Getting Started'], 'eloquent/relationships', + 'eloquent/soft-deleting', 'eloquent/nested-mutations', ] }, From 744c214924239ddb65bf7b5e21d908ee2574cda1 Mon Sep 17 00:00:00 2001 From: spawnia Date: Mon, 2 Sep 2019 20:29:06 +0200 Subject: [PATCH 15/19] Update definition docs --- docs/master/api-reference/directives.md | 32 +++++++----- .../Directives/SoftDeletesDirective.php | 13 ++++- src/Schema/Directives/TrashDirective.php | 3 ++ .../SoftDeletesAndTrashDirectiveTest.php | 51 +++++++++++++++++++ 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index 5853d7a144..69f1ce6925 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -2102,17 +2102,16 @@ query myQuery($someTest: Boolean) { ## @softDeletes -This directive adds an `trashed: Trash @trash` argument to you field, which allows you to define if `ONLY`, `WITH` -or `WITHOUT` trashed elements should be fetched. - -### Definition ```graphql +""" +Allows to filter if trashed elements should be fetched. +This manipulates the schema by adding the argument +`trashed: Trash @trash` to the field. +""" directive @softDeletes on FIELD_DEFINITION ``` -### Examples - -With following schema +The following schema definition from a `.graphql` file: ```graphql type Query { @@ -2120,14 +2119,16 @@ type Query { } ``` -It is possible to create queries like this: +Will result in a schema that looks like this: ```graphql -{ - tasks(trashed: ONLY) {...} +type Query { + tasks(trashed: Trash @trash): [Tasks!]! @all } ``` +Find out how the added filter works: [`@trash`](#trash) + ## @spread Spread out the nested values of an argument of type input object into it's parent. @@ -2217,14 +2218,17 @@ directive @subscription( ## @trash -Updates builder query to fetch only specified soft deleted elements. -This directive is used by [@softDeletes directive](#softdeletes) internally. - -### Definition ```graphql +""" +Allows to filter if trashed elements should be fetched. +""" directive @trash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION ``` +The most convenient way to use this directive is through [`@softDeletes`](#softdeletes). + +If you want to add it manually, make sure + ## @trim Run the `trim` function on an input value. diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/Schema/Directives/SoftDeletesDirective.php index 1ccd62d2a4..c0aa360fae 100644 --- a/src/Schema/Directives/SoftDeletesDirective.php +++ b/src/Schema/Directives/SoftDeletesDirective.php @@ -25,6 +25,11 @@ public function name(): string public static function definition(): string { return /* @lang GraphQL */ <<<'SDL' +""" +Allows to filter if trashed elements should be fetched. +This manipulates the schema by adding the argument +`trashed: Trash @trash` to the field. +""" directive @softDeletes on FIELD_DEFINITION SDL; } @@ -37,7 +42,13 @@ public static function definition(): string */ public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode &$parentType): void { - $softDeletesArgument = PartialParser::inputValueDefinition("\"Define if soft deleted models should be also fetched.\"\ntrashed: Trash @trash"); + $softDeletesArgument = PartialParser::inputValueDefinition(/* @lang GraphQL */ <<<'SDL' +""" +Allows to filter if trashed elements should be fetched. +""" +trashed: Trash @trashed +SDL + ); $fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, [$softDeletesArgument]); } } diff --git a/src/Schema/Directives/TrashDirective.php b/src/Schema/Directives/TrashDirective.php index 79bbeb871e..b4a61b4cb4 100644 --- a/src/Schema/Directives/TrashDirective.php +++ b/src/Schema/Directives/TrashDirective.php @@ -22,6 +22,9 @@ public function name(): string public static function definition(): string { return /* @lang GraphQL */ <<<'SDL' +""" +Allows to filter if trashed elements should be fetched. +""" directive @trash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION SDL; } diff --git a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php index 1435912ded..3cf53b5505 100644 --- a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php +++ b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php @@ -2,6 +2,7 @@ namespace Tests\Integration\Schema\Directives; +use Nuwave\Lighthouse\Schema\Directives\TrashDirective; use Tests\DBTestCase; use Tests\Utils\Models\Task; @@ -387,4 +388,54 @@ public function itCanBeUsedNested(): void ->assertJsonCount(2, 'data.user.tasksWithout') ->assertJsonCount(2, 'data.user.tasksSimple'); } + + /** + * @test + */ + public function itThrowsIfModelDoesNotSupportSoftDeletesTrash(): void + { + $this->schema = ' + type Query { + trash(trashed: Trash @trash): [User!]! @all + } + + type User { + id: ID + } + '; + + $this->expectExceptionMessage(TrashDirective::MODEL_MUST_USE_SOFT_DELETES); + $this->graphQL(' + { + softDeletes(trashed: WITH) { + id + } + } + '); + } + + /** + * @test + */ + public function itThrowsIfModelDoesNotSupportSoftDeletes(): void + { + $this->schema = ' + type Query { + softDeletes: [User!]! @all @softDeletes + } + + type User { + id: ID + } + '; + + $this->expectExceptionMessage(TrashDirective::MODEL_MUST_USE_SOFT_DELETES); + $this->graphQL(' + { + softDeletes(trashed: WITH) { + id + } + } + '); + } } From ac652507b864d1c58b7e93727e1c9d096de63603 Mon Sep 17 00:00:00 2001 From: spawnia Date: Mon, 2 Sep 2019 20:29:30 +0200 Subject: [PATCH 16/19] Throw if model does not use soft deletes --- .../Directives/SoftDeletesDirective.php | 2 +- src/Schema/Directives/TrashDirective.php | 25 ++++++++++--------- .../SoftDeletesAndTrashDirectiveTest.php | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/Schema/Directives/SoftDeletesDirective.php index c0aa360fae..6bbe10971a 100644 --- a/src/Schema/Directives/SoftDeletesDirective.php +++ b/src/Schema/Directives/SoftDeletesDirective.php @@ -46,7 +46,7 @@ public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefini """ Allows to filter if trashed elements should be fetched. """ -trashed: Trash @trashed +trashed: Trash @trash SDL ); $fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, [$softDeletesArgument]); diff --git a/src/Schema/Directives/TrashDirective.php b/src/Schema/Directives/TrashDirective.php index b4a61b4cb4..0ad9262c0f 100644 --- a/src/Schema/Directives/TrashDirective.php +++ b/src/Schema/Directives/TrashDirective.php @@ -2,13 +2,17 @@ namespace Nuwave\Lighthouse\Schema\Directives; +use Illuminate\Database\Eloquent\SoftDeletes; use Laravel\Scout\Builder as ScoutBuilder; use Illuminate\Database\Eloquent\Relations\Relation; +use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective; class TrashDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective { + const MODEL_MUST_USE_SOFT_DELETES = 'Use @trash only for Model classes that use the SoftDeletes trait.'; + /** * Name of the directive. * @@ -31,37 +35,34 @@ public static function definition(): string /** * Apply withTrashed, onlyTrashed or withoutTrashed to given $builder if needed. - * If builder model doesn't support soft deletes, this argument will be ignored! * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder - * @param mixed $value + * @param string|null $value "with", "without" or "only" * * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ public function handleBuilder($builder, $value) { - // skip execution, if model doesn't support soft delete if ($builder instanceof Relation) { $model = $builder->getRelated(); - $query = $builder->getQuery(); + } elseif($builder instanceof ScoutBuilder) { + $model = $builder->model; } else { - $model = $builder instanceof ScoutBuilder - ? $builder->model - : $builder->getModel(); - $query = $builder; + $model = $builder->getModel(); } - if (! in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses($model))) { - return $builder; + if (! in_array(SoftDeletes::class, class_uses_recursive($model))) { + throw new DefinitionException( + self::MODEL_MUST_USE_SOFT_DELETES + ); } - // apply trashed query modification if (! isset($value)) { return $builder; } $trashModificationMethod = "{$value}Trashed"; - $query->{$trashModificationMethod}(); + $builder->{$trashModificationMethod}(); return $builder; } diff --git a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php index 3cf53b5505..dfb55f19ba 100644 --- a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php +++ b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php @@ -407,7 +407,7 @@ public function itThrowsIfModelDoesNotSupportSoftDeletesTrash(): void $this->expectExceptionMessage(TrashDirective::MODEL_MUST_USE_SOFT_DELETES); $this->graphQL(' { - softDeletes(trashed: WITH) { + trash(trashed: WITH) { id } } From 9a5aa9cb462361322ce6976d74db74629b6728a0 Mon Sep 17 00:00:00 2001 From: spawnia Date: Mon, 2 Sep 2019 20:40:11 +0200 Subject: [PATCH 17/19] codestyle --- src/Schema/Directives/TrashDirective.php | 4 ++-- .../Schema/Directives/SoftDeletesAndTrashDirectiveTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Schema/Directives/TrashDirective.php b/src/Schema/Directives/TrashDirective.php index 0ad9262c0f..663cc31c78 100644 --- a/src/Schema/Directives/TrashDirective.php +++ b/src/Schema/Directives/TrashDirective.php @@ -2,8 +2,8 @@ namespace Nuwave\Lighthouse\Schema\Directives; -use Illuminate\Database\Eloquent\SoftDeletes; use Laravel\Scout\Builder as ScoutBuilder; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\Relation; use Nuwave\Lighthouse\Exceptions\DefinitionException; use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; @@ -45,7 +45,7 @@ public function handleBuilder($builder, $value) { if ($builder instanceof Relation) { $model = $builder->getRelated(); - } elseif($builder instanceof ScoutBuilder) { + } elseif ($builder instanceof ScoutBuilder) { $model = $builder->model; } else { $model = $builder->getModel(); diff --git a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php index dfb55f19ba..cbd688f79d 100644 --- a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php +++ b/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php @@ -2,9 +2,9 @@ namespace Tests\Integration\Schema\Directives; -use Nuwave\Lighthouse\Schema\Directives\TrashDirective; use Tests\DBTestCase; use Tests\Utils\Models\Task; +use Nuwave\Lighthouse\Schema\Directives\TrashDirective; class SoftDeletesAndTrashDirectiveTest extends DBTestCase { From 6df424314bf9df819ab7efe5157838f48a8610cc Mon Sep 17 00:00:00 2001 From: spawnia Date: Mon, 2 Sep 2019 20:50:23 +0200 Subject: [PATCH 18/19] trash -> trashed --- CHANGELOG.md | 3 ++- docs/master/api-reference/directives.md | 19 +++++++++++++------ docs/master/eloquent/soft-deleting.md | 13 +++++++++++-- src/Schema/AST/ASTBuilder.php | 10 +++++----- .../Directives/SoftDeletesDirective.php | 4 ++-- ...rashDirective.php => TrashedDirective.php} | 12 ++++++------ ...=> SoftDeletesAndTrashedDirectiveTest.php} | 14 +++++++------- 7 files changed, 46 insertions(+), 29 deletions(-) rename src/Schema/Directives/{TrashDirective.php => TrashedDirective.php} (80%) rename tests/Integration/Schema/Directives/{SoftDeletesAndTrashDirectiveTest.php => SoftDeletesAndTrashedDirectiveTest.php} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c9b38cd9..53a6d3e1b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/nuwave/lighthouse/compare/v4.2.0...master) -- Add `@softDeletes` and `@trash` directives to be able to fetch `onlyTrashed`, `withTrashed` or `withoutTrashed` models https://github.com/nuwave/lighthouse/pull/937 +- Add `@softDeletes` and `@trashed` directives to enable + filtering soft deleted models https://github.com/nuwave/lighthouse/pull/937 ## [4.2.0](https://github.com/nuwave/lighthouse/compare/v4.1.1...v4.2.0) diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index 69f1ce6925..d3a47b9b46 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -2106,7 +2106,7 @@ query myQuery($someTest: Boolean) { """ Allows to filter if trashed elements should be fetched. This manipulates the schema by adding the argument -`trashed: Trash @trash` to the field. +`trashed: Trashed @trashed` to the field. """ directive @softDeletes on FIELD_DEFINITION ``` @@ -2123,11 +2123,11 @@ Will result in a schema that looks like this: ```graphql type Query { - tasks(trashed: Trash @trash): [Tasks!]! @all + tasks(trashed: Trashed @trashed): [Tasks!]! @all } ``` -Find out how the added filter works: [`@trash`](#trash) +Find out how the added filter works: [`@trashed`](#trashed) ## @spread @@ -2216,18 +2216,25 @@ directive @subscription( ) on FIELD_DEFINITION ``` -## @trash +## @trashed ```graphql """ Allows to filter if trashed elements should be fetched. """ -directive @trash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +directive @trashed on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION ``` The most convenient way to use this directive is through [`@softDeletes`](#softdeletes). -If you want to add it manually, make sure +If you want to add it manually, make sure the argument is of the +enum type `Trashed`: + +```graphql +type Query { + flights(trashed: Trashed @trashed): [Flight!]! @all +} +``` ## @trim diff --git a/docs/master/eloquent/soft-deleting.md b/docs/master/eloquent/soft-deleting.md index 4cad04e9ef..161575acf3 100644 --- a/docs/master/eloquent/soft-deleting.md +++ b/docs/master/eloquent/soft-deleting.md @@ -16,11 +16,20 @@ type Query { ``` Lighthouse will add an argument `trashed` to the field definition -and automatically include the enum `Trash`. +and automatically include the enum `Trashed`. ```graphql type Query { - flights(trashed: Trash @trashed): [Flight!]! @all + flights(trashed: Trashed @trashed): [Flight!]! @all +} + +""" +Used for filtering +""" +enum Trashed { + ONLY @enum(value: "only") + WITH @enum(value: "with") + WITHOUT @enum(value: "without") } ``` diff --git a/src/Schema/AST/ASTBuilder.php b/src/Schema/AST/ASTBuilder.php index 4508f6eff4..2542b55580 100644 --- a/src/Schema/AST/ASTBuilder.php +++ b/src/Schema/AST/ASTBuilder.php @@ -95,7 +95,7 @@ public function build(): DocumentAST $this->addPaginationInfoTypes(); $this->addNodeSupport(); $this->addOrderByTypes(); - $this->addTrashEnum(); + $this->addTrashedEnum(); // Listeners may manipulate the DocumentAST that is passed by reference // into the ManipulateAST event. This can be useful for extensions @@ -364,17 +364,17 @@ enum SortOrder { } /** - * Add Trash enum that can be used in arguments with @paginate. + * Add Trashed enum to filter soft deleted models. * - * @see \Nuwave\Lighthouse\Schema\Directives\PaginateDirective + * @see \Nuwave\Lighthouse\Schema\Directives\TrashedDirective * * @return void */ - protected function addTrashEnum(): void + protected function addTrashedEnum(): void { $this->documentAST->setTypeDefinition( PartialParser::enumTypeDefinition(' - enum Trash { + enum Trashed { ONLY @enum(value: "only") WITH @enum(value: "with") WITHOUT @enum(value: "without") diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/Schema/Directives/SoftDeletesDirective.php index 6bbe10971a..346aa9c759 100644 --- a/src/Schema/Directives/SoftDeletesDirective.php +++ b/src/Schema/Directives/SoftDeletesDirective.php @@ -28,7 +28,7 @@ public static function definition(): string """ Allows to filter if trashed elements should be fetched. This manipulates the schema by adding the argument -`trashed: Trash @trash` to the field. +`trashed: Trashed @trashed` to the field. """ directive @softDeletes on FIELD_DEFINITION SDL; @@ -46,7 +46,7 @@ public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefini """ Allows to filter if trashed elements should be fetched. """ -trashed: Trash @trash +trashed: Trashed @trashed SDL ); $fieldDefinition->arguments = ASTHelper::mergeNodeList($fieldDefinition->arguments, [$softDeletesArgument]); diff --git a/src/Schema/Directives/TrashDirective.php b/src/Schema/Directives/TrashedDirective.php similarity index 80% rename from src/Schema/Directives/TrashDirective.php rename to src/Schema/Directives/TrashedDirective.php index 663cc31c78..672ee4a1b7 100644 --- a/src/Schema/Directives/TrashDirective.php +++ b/src/Schema/Directives/TrashedDirective.php @@ -9,9 +9,9 @@ use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective; -class TrashDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective +class TrashedDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective { - const MODEL_MUST_USE_SOFT_DELETES = 'Use @trash only for Model classes that use the SoftDeletes trait.'; + const MODEL_MUST_USE_SOFT_DELETES = 'Use @trashed only for Model classes that use the SoftDeletes trait.'; /** * Name of the directive. @@ -20,7 +20,7 @@ class TrashDirective extends BaseDirective implements ArgBuilderDirective, Defin */ public function name(): string { - return 'trash'; + return 'trashed'; } public static function definition(): string @@ -29,7 +29,7 @@ public static function definition(): string """ Allows to filter if trashed elements should be fetched. """ -directive @trash on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +directive @trashed on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION SDL; } @@ -61,8 +61,8 @@ public function handleBuilder($builder, $value) return $builder; } - $trashModificationMethod = "{$value}Trashed"; - $builder->{$trashModificationMethod}(); + $trashedModificationMethod = "{$value}Trashed"; + $builder->{$trashedModificationMethod}(); return $builder; } diff --git a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php b/tests/Integration/Schema/Directives/SoftDeletesAndTrashedDirectiveTest.php similarity index 96% rename from tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php rename to tests/Integration/Schema/Directives/SoftDeletesAndTrashedDirectiveTest.php index cbd688f79d..4e100413f2 100644 --- a/tests/Integration/Schema/Directives/SoftDeletesAndTrashDirectiveTest.php +++ b/tests/Integration/Schema/Directives/SoftDeletesAndTrashedDirectiveTest.php @@ -4,9 +4,9 @@ use Tests\DBTestCase; use Tests\Utils\Models\Task; -use Nuwave\Lighthouse\Schema\Directives\TrashDirective; +use Nuwave\Lighthouse\Schema\Directives\TrashedDirective; -class SoftDeletesAndTrashDirectiveTest extends DBTestCase +class SoftDeletesAndTrashedDirectiveTest extends DBTestCase { /** * @test @@ -392,11 +392,11 @@ public function itCanBeUsedNested(): void /** * @test */ - public function itThrowsIfModelDoesNotSupportSoftDeletesTrash(): void + public function itThrowsIfModelDoesNotSupportSoftDeletesTrashed(): void { $this->schema = ' type Query { - trash(trashed: Trash @trash): [User!]! @all + trashed(trashed: Trashed @trashed): [User!]! @all } type User { @@ -404,10 +404,10 @@ public function itThrowsIfModelDoesNotSupportSoftDeletesTrash(): void } '; - $this->expectExceptionMessage(TrashDirective::MODEL_MUST_USE_SOFT_DELETES); + $this->expectExceptionMessage(TrashedDirective::MODEL_MUST_USE_SOFT_DELETES); $this->graphQL(' { - trash(trashed: WITH) { + trashed(trashed: WITH) { id } } @@ -429,7 +429,7 @@ public function itThrowsIfModelDoesNotSupportSoftDeletes(): void } '; - $this->expectExceptionMessage(TrashDirective::MODEL_MUST_USE_SOFT_DELETES); + $this->expectExceptionMessage(TrashedDirective::MODEL_MUST_USE_SOFT_DELETES); $this->graphQL(' { softDeletes(trashed: WITH) { From 93410a360ecde2a88bcc900a2239f6494d6bae8d Mon Sep 17 00:00:00 2001 From: spawnia Date: Mon, 2 Sep 2019 23:17:41 +0200 Subject: [PATCH 19/19] changelog --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9291bd27f..6933c42ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/nuwave/lighthouse/compare/v4.2.1...master) +### Added + +- Add `@softDeletes` and `@trashed` directives to enable + filtering soft deleted models https://github.com/nuwave/lighthouse/pull/937 + ## [4.2.1](https://github.com/nuwave/lighthouse/compare/v4.2.0...v4.2.1) ### Fixed - Actually use the specified `edgeType` in Relay style connections https://github.com/nuwave/lighthouse/pull/939 -- Add `@softDeletes` and `@trashed` directives to enable - filtering soft deleted models https://github.com/nuwave/lighthouse/pull/937 - ## [4.2.0](https://github.com/nuwave/lighthouse/compare/v4.1.1...v4.2.0) ### Added