diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db7574020..65be3f987f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `@restore` and `@forceDelete` directives, similar to `@delete` https://github.com/nuwave/lighthouse/pull/941 - Add `@softDeletes` and `@trashed` directives to enable filtering soft deleted models https://github.com/nuwave/lighthouse/pull/937 @@ -16,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent throwing in `lighthouse:ide-helper` when no custom directives are defined https://github.com/nuwave/lighthouse/pull/948 +## Changed + +- Validate requirements for argument definitions of `@delete`, `@forceDelete` and `@restore` + during schema build time https://github.com/nuwave/lighthouse/pull/941 + ## [4.2.1](https://github.com/nuwave/lighthouse/compare/v4.2.0...v4.2.1) ### Fixed diff --git a/composer.json b/composer.json index 793ce203e1..954ce07580 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,8 @@ "extra": { "laravel": { "providers": [ - "Nuwave\\Lighthouse\\LighthouseServiceProvider" + "Nuwave\\Lighthouse\\LighthouseServiceProvider", + "Nuwave\\Lighthouse\\SoftDeletes\\SoftDeletesServiceProvider" ], "aliases": { "graphql": "Nuwave\\Lighthouse\\GraphQL" diff --git a/docs/master/api-reference/directives.md b/docs/master/api-reference/directives.md index d3a47b9b46..3b72407e75 100644 --- a/docs/master/api-reference/directives.md +++ b/docs/master/api-reference/directives.md @@ -630,20 +630,10 @@ type Mutation { ## @delete -Delete one or more models by their ID. - -```graphql -type Mutation { - deletePost(id: ID!): Post @delete -} -``` - -### Definition - ```graphql """ Delete one or more models by their ID. -The field must have an single non-null argument that may be a list. +The field must have a single non-null argument that may be a list. """ directive @delete( """ @@ -651,9 +641,23 @@ directive @delete( If set to `false`, regular non-global ids are used. """ globalId: Boolean = false + + """ + Specify the class name of the model to use. + This is only needed when the default model resolution does not work. + """ + model: String ) on FIELD_DEFINITION ``` +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + deletePost(id: ID!): Post @delete +} +``` + ### Examples If you use global ids, you can set the `globalId` argument to `true`. @@ -669,6 +673,8 @@ You can also delete multiple models at once. Define a field that takes a list of IDs and returns a Collection of the deleted models. +_In contrast to Laravel mass updates, this does trigger model events._ + ```graphql type Mutation { deletePosts(id: [ID!]!): [Post!]! @delete @@ -856,6 +862,38 @@ type Query { } ``` +## @forceDelete + +```graphql +""" +Permanently remove one or more soft deleted models by their ID. +The field must have a single non-null argument that may be a list. +""" +directive @forceDelete( + """ + Set to `true` to use global ids for finding the model. + If set to `false`, regular non-global ids are used. + """ + globalId: Boolean = false + + """ + Specify the class name of the model to use. + This is only needed when the default model resolution does not work. + """ + model: String +) on FIELD_DEFINITION +``` + +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + forceDeletePost(id: ID!): Post @forceDelete +} +``` + +Works very similar to the [`@delete`](#delete) directive. + ## @enum Assign an internal value to an enum key. When dealing with the Enum type in your code, @@ -1918,6 +1956,38 @@ directive @rename( ) on FIELD_DEFINITION ``` +## @restore + +```graphql +""" +Un-delete one or more soft deleted models by their ID. +The field must have a single non-null argument that may be a list. +""" +directive @restore( + """ + Set to `true` to use global ids for finding the model. + If set to `false`, regular non-global ids are used. + """ + globalId: Boolean = false + + """ + Specify the class name of the model to use. + This is only needed when the default model resolution does not work. + """ + model: String +) on FIELD_DEFINITION +``` + +Use it on a root mutation field that returns an instance of the Model. + +```graphql +type Mutation { + restorePost(id: ID!): Post @restore +} +``` + +Works very similar to the [`@delete`](#delete) directive. + ## @rules Validate an argument using [Laravel built-in validation](https://laravel.com/docs/validation). diff --git a/docs/master/eloquent/soft-deleting.md b/docs/master/eloquent/soft-deleting.md index 161575acf3..0d1a70deae 100644 --- a/docs/master/eloquent/soft-deleting.md +++ b/docs/master/eloquent/soft-deleting.md @@ -6,7 +6,7 @@ Lighthouse offers convenient helpers to work with models that utilize ## 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 +you can add the [`@softDeletes`](../api-reference/directives.md#softdeletes) directive to a field to be able to query `onlyTrashed`, `withTrashed` or `withoutTrashed` elements. ```graphql @@ -15,8 +15,8 @@ type Query { } ``` -Lighthouse will add an argument `trashed` to the field definition -and automatically include the enum `Trashed`. +Lighthouse will automatically add an argument `trashed` to the field definition +and include the enum `Trashed`. ```graphql type Query { @@ -42,3 +42,60 @@ You can include soft deleted models in your result with a query like this: } } ``` + +## Restoring Soft Deleted Models + +If your model uses the `Illuminate\Database\Eloquent\SoftDeletes` trait, +you can restore your model using the [`@restore`](../api-reference/directives.md#restore) directive. + +```graphql +type Mutation { + restoreFlight(id: ID!): Flight @restore +} +``` + +Simply call the field with the ID of the flight you want to restore. + +```graphql +mutation { + restoreFlight(id: 1) { + id + } +} +``` + +This mutation will return the restored object. + +## Permanently Deleting Models + +To truly remove model from database, +use the [@forceDelete](../api-reference/directives.md#forcedelete) directive. +Your model must use the `Illuminate\Database\Eloquent\SoftDeletes` trait. + +```graphql +type Mutation { + forceDeleteFlight(id: ID!): Flight @forceDelete +} +``` + +Simply call it with the ID of the Flight you want to permanently remove. + +```graphql +mutation { + forceDeleteFlight(id: 5){ + id + } +} +``` + +This mutation will return the deleted object, so you will have a last chance to look at the data. + +```json +{ + "data": { + "forceDeleteFlight": { + "id": 5 + } + } +} +``` diff --git a/src/Console/IdeHelperCommand.php b/src/Console/IdeHelperCommand.php index 8beeb10ace..547631de52 100644 --- a/src/Console/IdeHelperCommand.php +++ b/src/Console/IdeHelperCommand.php @@ -5,7 +5,7 @@ use Illuminate\Console\Command; use HaydenPierce\ClassFinder\ClassFinder; use Nuwave\Lighthouse\Schema\AST\PartialParser; -use Nuwave\Lighthouse\Schema\DirectiveNamespaces; +use Nuwave\Lighthouse\Schema\DirectiveNamespacer; use Nuwave\Lighthouse\Support\Contracts\Directive; use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; use HaydenPierce\ClassFinder\Exception\ClassFinderException; @@ -36,10 +36,10 @@ class IdeHelperCommand extends Command /** * Execute the console command. * - * @param \Nuwave\Lighthouse\Schema\DirectiveNamespaces $directiveNamespaces + * @param \Nuwave\Lighthouse\Schema\DirectiveNamespacer $directiveNamespaces * @return int */ - public function handle(DirectiveNamespaces $directiveNamespaces): int + public function handle(DirectiveNamespacer $directiveNamespaces): int { if (! class_exists('HaydenPierce\ClassFinder\ClassFinder')) { $this->error( diff --git a/src/Schema/AST/ASTBuilder.php b/src/Schema/AST/ASTBuilder.php index 2542b55580..79b46e1284 100644 --- a/src/Schema/AST/ASTBuilder.php +++ b/src/Schema/AST/ASTBuilder.php @@ -95,7 +95,6 @@ public function build(): DocumentAST $this->addPaginationInfoTypes(); $this->addNodeSupport(); $this->addOrderByTypes(); - $this->addTrashedEnum(); // Listeners may manipulate the DocumentAST that is passed by reference // into the ManipulateAST event. This can be useful for extensions @@ -362,24 +361,4 @@ enum SortOrder { ') ); } - - /** - * Add Trashed enum to filter soft deleted models. - * - * @see \Nuwave\Lighthouse\Schema\Directives\TrashedDirective - * - * @return void - */ - protected function addTrashedEnum(): void - { - $this->documentAST->setTypeDefinition( - PartialParser::enumTypeDefinition(' - enum Trashed { - ONLY @enum(value: "only") - WITH @enum(value: "with") - WITHOUT @enum(value: "without") - } - ') - ); - } } diff --git a/src/Schema/DirectiveNamespaces.php b/src/Schema/DirectiveNamespacer.php similarity index 98% rename from src/Schema/DirectiveNamespaces.php rename to src/Schema/DirectiveNamespacer.php index 4884a0fe28..61e94f7036 100644 --- a/src/Schema/DirectiveNamespaces.php +++ b/src/Schema/DirectiveNamespacer.php @@ -6,7 +6,7 @@ use Illuminate\Contracts\Events\Dispatcher; use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces; -class DirectiveNamespaces +class DirectiveNamespacer { /** * @var \Illuminate\Contracts\Events\Dispatcher diff --git a/src/Schema/Directives/DeleteDirective.php b/src/Schema/Directives/DeleteDirective.php index aa3c1b4a6d..e6d01c7bcb 100644 --- a/src/Schema/Directives/DeleteDirective.php +++ b/src/Schema/Directives/DeleteDirective.php @@ -2,36 +2,11 @@ namespace Nuwave\Lighthouse\Schema\Directives; -use GraphQL\Language\AST\NodeKind; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Collection; -use Nuwave\Lighthouse\Schema\Values\FieldValue; -use Nuwave\Lighthouse\Support\Contracts\GlobalId; -use GraphQL\Language\AST\InputValueDefinitionNode; -use Nuwave\Lighthouse\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\DefinedDirective; -class DeleteDirective extends BaseDirective implements FieldResolver, DefinedDirective +class DeleteDirective extends ModifyModelExistenceDirective implements DefinedDirective { - /** - * The GlobalId resolver. - * - * @var \Nuwave\Lighthouse\Support\Contracts\GlobalId - */ - protected $globalId; - - /** - * DeleteDirective constructor. - * - * @param \Nuwave\Lighthouse\Support\Contracts\GlobalId $globalId - * @return void - */ - public function __construct(GlobalId $globalId) - { - $this->globalId = $globalId; - } - /** * Name of the directive. * @@ -47,7 +22,7 @@ public static function definition(): string return /* @lang GraphQL */ <<<'SDL' """ Delete one or more models by their ID. -The field must have an single non-null argument that may be a list. +The field must have a single non-null argument that may be a list. """ directive @delete( """ @@ -55,80 +30,36 @@ public static function definition(): string If set to `false`, regular non-global ids are used. """ globalId: Boolean = false + + """ + Specify the class name of the model to use. + This is only needed when the default model resolution does not work. + """ + model: String ) on FIELD_DEFINITION SDL; } /** - * Resolve the field directive. + * Find one or more models by id. * - * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue - * @return \Nuwave\Lighthouse\Schema\Values\FieldValue + * @param string|\Illuminate\Database\Eloquent\Model $modelClass + * @param string|int|string[]|int[] $idOrIds + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection */ - public function resolveField(FieldValue $fieldValue): FieldValue + protected function find(string $modelClass, $idOrIds) { - return $fieldValue->setResolver( - function ($root, array $args) { - $argumentDefinition = $this->getSingleArgumentDefinition(); - - if ($argumentDefinition->type->kind !== NodeKind::NON_NULL_TYPE) { - throw new DirectiveException( - "The @delete directive requires the field {$this->definitionNode->name->value} to have a NonNull argument. Mark it with !" - ); - } - - /** @var string|int|string[] $idOrIds */ - $idOrIds = reset($args); - if ($this->directiveArgValue('globalId', false)) { - // At this point we know the type is at least wrapped in a NonNull type, so we go one deeper - if ($argumentDefinition->type->type->kind === NodeKind::LIST_TYPE) { - $idOrIds = array_map( - function (string $id): string { - return $this->globalId->decodeID($id); - }, - $idOrIds - ); - } else { - $idOrIds = $this->globalId->decodeID($idOrIds); - } - } - - /** @var \Illuminate\Database\Eloquent\Model $modelClass */ - $modelClass = $this->getModelClass(); - $model = $modelClass::find($idOrIds); - - if (! $model) { - return; - } - - if ($model instanceof Model) { - $model->delete(); - } - - if ($model instanceof Collection) { - $modelClass::destroy($idOrIds); - } - - return $model; - } - ); + return $modelClass::find($idOrIds); } /** - * Ensure there is only a single argument defined on the field. + * Bring a model in or out of existence. * - * @return \GraphQL\Language\AST\InputValueDefinitionNode - * - * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException + * @param \Illuminate\Database\Eloquent\Model $model + * @return void */ - protected function getSingleArgumentDefinition(): InputValueDefinitionNode + protected function modifyExistence(Model $model): void { - if (count($this->definitionNode->arguments) !== 1) { - throw new DirectiveException( - "The @delete directive requires the field {$this->definitionNode->name->value} to only contain a single argument." - ); - } - - return $this->definitionNode->arguments[0]; + $model->delete(); } } diff --git a/src/Schema/Directives/ModifyModelExistenceDirective.php b/src/Schema/Directives/ModifyModelExistenceDirective.php new file mode 100644 index 0000000000..83b3afea11 --- /dev/null +++ b/src/Schema/Directives/ModifyModelExistenceDirective.php @@ -0,0 +1,146 @@ +globalId = $globalId; + } + + /** + * Resolve the field directive. + * + * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue + * @return \Nuwave\Lighthouse\Schema\Values\FieldValue + */ + public function resolveField(FieldValue $fieldValue): FieldValue + { + return $fieldValue->setResolver( + function ($root, array $args) { + /** @var string|int|string[]|int[] $idOrIds */ + $idOrIds = reset($args); + + if ($this->directiveArgValue('globalId', false)) { + // At this point we know the type is at least wrapped in a NonNull type, so we go one deeper + if ($this->idArgument()->type instanceof ListTypeNode) { + $idOrIds = array_map( + function (string $id): string { + return $this->globalId->decodeID($id); + }, + $idOrIds + ); + } else { + $idOrIds = $this->globalId->decodeID($idOrIds); + } + } + + $modelOrModels = $this->find( + $this->getModelClass(), + $idOrIds + ); + + if (! $modelOrModels) { + return; + } + + if ($modelOrModels instanceof Model) { + $this->modifyExistence($modelOrModels); + } + + if ($modelOrModels instanceof Collection) { + foreach ($modelOrModels as $model) { + $this->modifyExistence($model); + } + } + + return $modelOrModels; + } + ); + } + + /** + * Get the type of the id argument. + * + * Not using an actual type hint, as the manipulateFieldDefinition function + * validates the type during schema build time.f + * + * @return \GraphQL\Language\AST\NonNullTypeNode + */ + protected function idArgument() + { + return $this->definitionNode->arguments[0]->type; + } + + /** + * @param DocumentAST $documentAST + * @param FieldDefinitionNode $fieldDefinition + * @param ObjectTypeDefinitionNode $parentType + * @return void + * + * @throws \Nuwave\Lighthouse\Exceptions\DirectiveException + */ + public function manipulateFieldDefinition( + DocumentAST &$documentAST, + FieldDefinitionNode &$fieldDefinition, + ObjectTypeDefinitionNode &$parentType + ): void { + // Ensure there is only a single argument defined on the field. + if (count($this->definitionNode->arguments) !== 1) { + throw new DirectiveException( + 'The @'.static::name()." directive requires the field {$this->definitionNode->name->value} to only contain a single argument." + ); + } + + if (! $this->idArgument() instanceof NonNullTypeNode) { + throw new DirectiveException( + 'The @'.static::name()." directive requires the field {$this->definitionNode->name->value} to have a NonNull argument. Mark it with !" + ); + } + } + + /** + * Find one or more models by id. + * + * @param string|\Illuminate\Database\Eloquent\Model $modelClass + * @param string|int|string[]|int[] $idOrIds + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection + */ + abstract protected function find(string $modelClass, $idOrIds); + + /** + * Bring a model in or out of existence. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return void + */ + abstract protected function modifyExistence(Model $model): void; +} diff --git a/src/Schema/Factories/DirectiveFactory.php b/src/Schema/Factories/DirectiveFactory.php index f7237a233a..65d8e6ad2d 100644 --- a/src/Schema/Factories/DirectiveFactory.php +++ b/src/Schema/Factories/DirectiveFactory.php @@ -7,7 +7,7 @@ use GraphQL\Language\AST\Node; use Illuminate\Support\Collection; use GraphQL\Language\AST\DirectiveNode; -use Nuwave\Lighthouse\Schema\DirectiveNamespaces; +use Nuwave\Lighthouse\Schema\DirectiveNamespacer; use Nuwave\Lighthouse\Support\Contracts\Directive; use Nuwave\Lighthouse\Exceptions\DirectiveException; use Nuwave\Lighthouse\Schema\Directives\BaseDirective; @@ -35,17 +35,22 @@ class DirectiveFactory * * @var string[] */ - protected $directiveNamespaces = []; + protected $directiveNamespaces; + + /** + * @var DirectiveNamespacer + */ + protected $directiveNamespacer; /** * DirectiveFactory constructor. * - * @param \Nuwave\Lighthouse\Schema\DirectiveNamespaces $directiveNamespaces + * @param \Nuwave\Lighthouse\Schema\DirectiveNamespacer $directiveNamespacer * @return void */ - public function __construct(DirectiveNamespaces $directiveNamespaces) + public function __construct(DirectiveNamespacer $directiveNamespacer) { - $this->directiveNamespaces = $directiveNamespaces->gather(); + $this->directiveNamespacer = $directiveNamespacer; } /** @@ -88,6 +93,10 @@ protected function resolve(string $directiveName): ?Directive */ protected function createOrFail(string $directiveName): Directive { + if (! $this->directiveNamespaces) { + $this->directiveNamespaces = $this->directiveNamespacer->gather(); + } + foreach ($this->directiveNamespaces as $baseNamespace) { $className = $baseNamespace.'\\'.Str::studly($directiveName).'Directive'; if (class_exists($className)) { diff --git a/src/SoftDeletes/ForceDeleteDirective.php b/src/SoftDeletes/ForceDeleteDirective.php new file mode 100644 index 0000000000..89c90ac915 --- /dev/null +++ b/src/SoftDeletes/ForceDeleteDirective.php @@ -0,0 +1,89 @@ +find($idOrIds); + } + + /** + * Bring a model in or out of existence. + * + * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $model + * @return void + */ + protected function modifyExistence(Model $model): void + { + $model->forceDelete(); + } + + /** + * Manipulate the AST based on a field definition. + * + * @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST + * @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition + * @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType + * @return void + */ + public function manipulateFieldDefinition( + DocumentAST &$documentAST, + FieldDefinitionNode &$fieldDefinition, + ObjectTypeDefinitionNode &$parentType + ): void { + parent::manipulateFieldDefinition($documentAST, $fieldDefinition, $parentType); + + SoftDeletesServiceProvider::assertModelUsesSoftDeletes($this->getModelClass(), + self::MODEL_NOT_USING_SOFT_DELETES); + } +} diff --git a/src/SoftDeletes/RestoreDirective.php b/src/SoftDeletes/RestoreDirective.php new file mode 100644 index 0000000000..e5bef607d1 --- /dev/null +++ b/src/SoftDeletes/RestoreDirective.php @@ -0,0 +1,91 @@ +find($idOrIds); + } + + /** + * Bring a model in or out of existence. + * + * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $model + * @return void + */ + protected function modifyExistence(Model $model): void + { + $model->restore(); + } + + /** + * Manipulate the AST based on a field definition. + * + * @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST + * @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition + * @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType + * @return void + */ + public function manipulateFieldDefinition( + DocumentAST &$documentAST, + FieldDefinitionNode &$fieldDefinition, + ObjectTypeDefinitionNode &$parentType + ): void { + parent::manipulateFieldDefinition($documentAST, $fieldDefinition, $parentType); + + SoftDeletesServiceProvider::assertModelUsesSoftDeletes($this->getModelClass(), + self::MODEL_NOT_USING_SOFT_DELETES); + } +} diff --git a/src/Schema/Directives/SoftDeletesDirective.php b/src/SoftDeletes/SoftDeletesDirective.php similarity index 94% rename from src/Schema/Directives/SoftDeletesDirective.php rename to src/SoftDeletes/SoftDeletesDirective.php index 346aa9c759..6e38945f12 100644 --- a/src/Schema/Directives/SoftDeletesDirective.php +++ b/src/SoftDeletes/SoftDeletesDirective.php @@ -1,12 +1,13 @@ listen( + ManipulateAST::class, + function (ManipulateAST $manipulateAST): void { + $manipulateAST->documentAST + ->setTypeDefinition( + PartialParser::enumTypeDefinition(' + enum Trashed { + ONLY @enum(value: "only") + WITH @enum(value: "with") + WITHOUT @enum(value: "without") + } + ') + ); + } + ); + + $dispatcher->listen( + RegisterDirectiveNamespaces::class, + function (RegisterDirectiveNamespaces $registerDirectiveNamespaces): string { + return __NAMESPACE__; + } + ); + } +} diff --git a/src/Schema/Directives/TrashedDirective.php b/src/SoftDeletes/TrashedDirective.php similarity index 95% rename from src/Schema/Directives/TrashedDirective.php rename to src/SoftDeletes/TrashedDirective.php index 672ee4a1b7..1b27315f8f 100644 --- a/src/Schema/Directives/TrashedDirective.php +++ b/src/SoftDeletes/TrashedDirective.php @@ -1,11 +1,12 @@ expectException(DirectiveException::class); - $this->schema = ' + $this->buildSchema(' type User { id: ID! name: String } - type Mutation { + type Query { deleteUser(id: ID): User @delete } - '.$this->placeholderQuery(); - - $this->graphQL(' - mutation { - deleteUser(id: 1) { - name - } - } '); } @@ -105,23 +97,14 @@ public function itRejectsDefinitionWithNoArgument(): void { $this->expectException(DirectiveException::class); - $this->schema = ' + $this->buildSchema(' type User { id: ID! - name: String } - type Mutation { + type Query { deleteUser: User @delete } - '.$this->placeholderQuery(); - - $this->graphQL(' - mutation { - deleteUser { - name - } - } '); } @@ -132,23 +115,14 @@ public function itRejectsDefinitionWithMultipleArguments(): void { $this->expectException(DirectiveException::class); - $this->schema = ' + $this->buildSchema(' type User { id: ID! - name: String } - type Mutation { + type Query { deleteUser(foo: String, bar: Int): User @delete } - '.$this->placeholderQuery(); - - $this->graphQL(' - mutation { - deleteUser { - name - } - } '); } } diff --git a/tests/Integration/SoftDeletes/ForceDeleteDirectiveTest.php b/tests/Integration/SoftDeletes/ForceDeleteDirectiveTest.php new file mode 100644 index 0000000000..a8671738a7 --- /dev/null +++ b/tests/Integration/SoftDeletes/ForceDeleteDirectiveTest.php @@ -0,0 +1,180 @@ +create(); + + $this->schema = ' + type Task { + id: ID! + } + + type Mutation { + forceDeleteTask(id: ID!): Task @forceDelete + } + '.$this->placeholderQuery(); + + $this->graphQL(' + mutation { + forceDeleteTask(id: 1) { + id + } + } + ')->assertJson([ + 'data' => [ + 'forceDeleteTask' => [ + 'id' => 1, + ], + ], + ]); + + $this->assertCount(0, Task::withTrashed()->get()); + } + + /** + * @test + */ + public function itForceDeletesDeletedTaskAndReturnsIt(): void + { + $task = factory(Task::class)->create(); + $task->delete(); + + $this->schema = ' + type Task { + id: ID! + } + + type Mutation { + forceDeleteTask(id: ID!): Task @forceDelete + } + '.$this->placeholderQuery(); + + $this->graphQL(' + mutation { + forceDeleteTask(id: 1) { + id + } + } + ')->assertJson([ + 'data' => [ + 'forceDeleteTask' => [ + 'id' => 1, + ], + ], + ]); + + $this->assertCount(0, Task::withTrashed()->get()); + } + + /** + * @test + */ + public function itForceDeletesMultipleTasksAndReturnsThem(): void + { + factory(Task::class, 2)->create(); + + $this->schema = ' + type Task { + id: ID! + name: String + } + + type Mutation { + forceDeleteTasks(id: [ID!]!): [Task!]! @forceDelete + } + '.$this->placeholderQuery(); + + $this->graphQL(' + mutation { + forceDeleteTasks(id: [1, 2]) { + name + } + } + ')->assertJsonCount(2, 'data.forceDeleteTasks'); + + $this->assertCount(0, Task::withTrashed()->get()); + } + + /** + * @test + */ + public function itRejectsDefinitionWithNullableArgument(): void + { + $this->expectException(DirectiveException::class); + + $this->buildSchema(' + type Task { + id: ID! + } + + type Query { + deleteTask(id: ID): Task @forceDelete + } + '); + } + + /** + * @test + */ + public function itRejectsDefinitionWithNoArgument(): void + { + $this->expectException(DirectiveException::class); + + $this->buildSchema(' + type Task { + id: ID! + } + + type Query { + deleteTask: Task @forceDelete + } + '); + } + + /** + * @test + */ + public function itRejectsDefinitionWithMultipleArguments(): void + { + $this->expectException(DirectiveException::class); + + $this->buildSchema(' + type Task { + id: ID! + } + + type Query { + deleteTask(foo: String, bar: Int): Task @forceDelete + } + '); + } + + /** + * @test + */ + public function itRejectsUsingDirectiveWithNoSoftDeleteModels(): void + { + $this->expectExceptionMessage(ForceDeleteDirective::MODEL_NOT_USING_SOFT_DELETES); + $this->buildSchema(' + type User { + id: ID! + } + + type Query { + deleteUser(id: ID!): User @forceDelete + } + '); + } +} diff --git a/tests/Integration/SoftDeletes/RestoreDirectiveTest.php b/tests/Integration/SoftDeletes/RestoreDirectiveTest.php new file mode 100644 index 0000000000..cda3970408 --- /dev/null +++ b/tests/Integration/SoftDeletes/RestoreDirectiveTest.php @@ -0,0 +1,156 @@ +create(); + $task->delete(); + + $this->assertCount(1, Task::withTrashed()->get()); + $this->assertCount(0, Task::withoutTrashed()->get()); + + $this->schema = ' + type Task { + id: ID! + } + + type Mutation { + restoreTask(id: ID!): Task @restore + } + '.$this->placeholderQuery(); + + $this->graphQL(' + mutation { + restoreTask(id: 1) { + id + } + } + ')->assertJson([ + 'data' => [ + 'restoreTask' => [ + 'id' => 1, + ], + ], + ]); + + $this->assertCount(1, Task::withoutTrashed()->get()); + } + + /** + * @test + */ + public function itRestoresMultipleTasksAndReturnsThem(): void + { + $tasks = factory(Task::class, 2)->create(); + foreach ($tasks as $task) { + $task->delete(); + } + + $this->assertCount(2, Task::withTrashed()->get()); + $this->assertCount(0, Task::withoutTrashed()->get()); + + $this->schema = ' + type Task { + id: ID! + name: String + } + + type Mutation { + restoreTasks(id: [ID!]!): [Task!]! @restore + } + '.$this->placeholderQuery(); + + $this->graphQL(' + mutation { + restoreTasks(id: [1, 2]) { + name + } + } + ')->assertJsonCount(2, 'data.restoreTasks'); + + $this->assertCount(2, Task::withoutTrashed()->get()); + } + + /** + * @test + */ + public function itRejectsDefinitionWithNullableArgument(): void + { + $this->expectException(DirectiveException::class); + + $this->buildSchema(' + type Task { + id: ID! + } + + type Query { + restoreTask(id: ID): Task @restore + } + '); + } + + /** + * @test + */ + public function itRejectsDefinitionWithNoArgument(): void + { + $this->expectException(DirectiveException::class); + + $this->buildSchema(' + type Task { + id: ID! + } + + type Query { + restoreTask: Task @restore + } + '); + } + + /** + * @test + */ + public function itRejectsDefinitionWithMultipleArguments(): void + { + $this->expectException(DirectiveException::class); + + $this->buildSchema(' + type Task { + id: ID! + } + + type Query { + restoreTask(foo: String, bar: Int): Task @restore + } + '); + } + + /** + * @test + */ + public function itRejectsUsingDirectiveWithNoSoftDeleteModels(): void + { + $this->expectExceptionMessage(RestoreDirective::MODEL_NOT_USING_SOFT_DELETES); + + $this->buildSchema(' + type User { + id: ID! + } + + type Query { + restoreUser(id: ID!): User @restore + } + '); + } +} diff --git a/tests/Integration/Schema/Directives/SoftDeletesAndTrashedDirectiveTest.php b/tests/Integration/SoftDeletes/SoftDeletesAndTrashedDirectiveTest.php similarity index 98% rename from tests/Integration/Schema/Directives/SoftDeletesAndTrashedDirectiveTest.php rename to tests/Integration/SoftDeletes/SoftDeletesAndTrashedDirectiveTest.php index 4e100413f2..c4f07d3105 100644 --- a/tests/Integration/Schema/Directives/SoftDeletesAndTrashedDirectiveTest.php +++ b/tests/Integration/SoftDeletes/SoftDeletesAndTrashedDirectiveTest.php @@ -1,10 +1,10 @@