Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement @forceDelete and @restore directives #941

Merged
merged 27 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5528ce0
implement @forceDelete and @restore directives
lorado Aug 30, 2019
4983089
update link to PR
lorado Aug 30, 2019
6b16c3d
refactor delete, restore and forceDelete to reduce duplicated code
lorado Aug 30, 2019
0131dc4
fix ternary - use `withTrashed` on forceDelete too
lorado Aug 30, 2019
c5573ae
Add missing test - forceDelete when model is already soft deleted
lorado Aug 30, 2019
93e16bf
fix code style
lorado Aug 30, 2019
62414a7
Merge remote-tracking branch 'master/master' into implement-restore-a…
lorado Aug 30, 2019
c078572
Merge from master, set directives definitions
lorado Aug 30, 2019
9b5e74b
Make the code and docs more explicit
spawnia Sep 2, 2019
b7263a5
merge
spawnia Sep 2, 2019
5dd77a2
Merge branch 'master' into implement-restore-and-forceDelete
spawnia Sep 2, 2019
dcc5126
Merge branch 'master' into implement-restore-and-forceDelete
spawnia Sep 2, 2019
49514bc
Fix changelog
spawnia Sep 2, 2019
37075ca
Fix directives
spawnia Sep 2, 2019
805acb6
style
spawnia Sep 2, 2019
1b1c7d7
Merge remote-tracking branch 'master/master' into implement-restore-a…
lorado Sep 2, 2019
f3131a1
Merge remote-tracking branch 'master/master' into implement-restore-a…
lorado Sep 2, 2019
9f13cf0
@forceDelete and @restore now verify usage of SoftDeletes
lorado Sep 2, 2019
cfc46e0
update docs
lorado Sep 2, 2019
039e291
fix style
lorado Sep 2, 2019
5d2d69b
fix style
lorado Sep 2, 2019
373a113
Merge branch 'master' into implement-restore-and-forceDelete
spawnia Sep 3, 2019
9310e21
Validate requirements for argument definitions of `@delete`, `@forceD…
spawnia Sep 3, 2019
3b9f47f
Factor out SoftDeletes into module
spawnia Sep 3, 2019
6f93e5e
style
spawnia Sep 3, 2019
f412673
Include SoftDeletesServiceProvider by default
spawnia Sep 3, 2019
0311526
fix style
lorado Sep 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Validate requirements for argument definitions of @delete, `@forceD…
…elete` and `@restore` during schema build time
  • Loading branch information
spawnia committed Sep 3, 2019
commit 9310e21bd27e9c96159c074a0b43e23f318e8ea6
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,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
Expand Down
18 changes: 10 additions & 8 deletions docs/master/eloquent/soft-deleting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -45,18 +45,19 @@ 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 [`@restore`](../api-reference/directives.md#restore) directive.
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 Query {
type Mutation {
restoreFlight(id: ID!): Flight @restore
}
```

Simply call it with the ID of the flight you want to restore.
Simply call the field with the ID of the flight you want to restore.

```graphql
{
mutation {
restoreFlight(id: 1) {
id
}
Expand All @@ -67,7 +68,9 @@ This mutation will return the restored object.

## Permanently Deleting Models

To truly remove model from database, use [@forceDelete](../api-reference/directives.md#forcedelete) directive. Your model must use the `Illuminate\Database\Eloquent\SoftDeletes` trait.
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 {
Expand Down Expand Up @@ -96,4 +99,3 @@ This mutation will return the deleted object, so you will have a last chance to
}
}
```

33 changes: 27 additions & 6 deletions src/Schema/Directives/ForceDeleteDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

namespace Nuwave\Lighthouse\Schema\Directives;

use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\SoftDeletes\Utils;

class ForceDeleteDirective extends ModifyModelExistenceDirective implements DefinedDirective
class ForceDeleteDirective extends ModifyModelExistenceDirective
{
protected $verifySoftDeletesUsed = true;
const MODEL_NOT_USING_SOFT_DELETES = 'Use the @forceDelete directive only for Model classes that use the SoftDeletes trait.';

/**
* Name of the directive.
Expand Down Expand Up @@ -45,8 +48,8 @@ public static function definition(): string
/**
* Find one or more models by id.
*
* @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $modelClass
* @param string|int|string[]|int[] $idOrIds
* @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $modelClass
* @param string|int|string[]|int[] $idOrIds
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
*/
protected function find(string $modelClass, $idOrIds)
Expand All @@ -57,11 +60,29 @@ protected function find(string $modelClass, $idOrIds)
/**
* Bring a model in or out of existence.
*
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $model
* @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);

Utils::assertModelUsesSoftDeletes($this->getModelClass(), self::MODEL_NOT_USING_SOFT_DELETES);
}
}
83 changes: 33 additions & 50 deletions src/Schema/Directives/ModifyModelExistenceDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,23 @@

namespace Nuwave\Lighthouse\Schema\Directives;

use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Illuminate\Database\Eloquent\Model;
use GraphQL\Language\AST\NonNullTypeNode;
use Illuminate\Database\Eloquent\Collection;
use GraphQL\Language\AST\FieldDefinitionNode;
use Illuminate\Database\Eloquent\SoftDeletes;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\GlobalId;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;

abstract class ModifyModelExistenceDirective extends BaseDirective implements FieldResolver, FieldManipulator
abstract class ModifyModelExistenceDirective extends BaseDirective implements FieldResolver, FieldManipulator, DefinedDirective
{
/**
* The GlobalId resolver.
*
* @var bool
*/
protected $verifySoftDeletesUsed = false;

/**
* The GlobalId resolver.
*
Expand Down Expand Up @@ -54,21 +47,12 @@ public function resolveField(FieldValue $fieldValue): FieldValue
{
return $fieldValue->setResolver(
function ($root, array $args) {
$argumentDefinition = $this->getSingleArgumentDefinition();

$argumentType = $argumentDefinition->type;
if (! $argumentType instanceof NonNullTypeNode) {
throw new DirectiveException(
'The @'.static::name()." directive requires the field {$this->definitionNode->name->value} to have a NonNull argument. Mark it with !"
);
}

/** @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 ($argumentType->type instanceof ListTypeNode) {
if ($this->idArgument()->type instanceof ListTypeNode) {
$idOrIds = array_map(
function (string $id): string {
return $this->globalId->decodeID($id);
Expand Down Expand Up @@ -105,21 +89,43 @@ function (string $id): string {
}

/**
* Ensure there is only a single argument defined on the field.
* Get the type of the id argument.
*
* @return \GraphQL\Language\AST\InputValueDefinitionNode
* Not using an actual type hint, as the manipulateFieldDefinition function
* validates the type during schema build time.f
*
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
* @return \GraphQL\Language\AST\NonNullTypeNode
*/
protected function getSingleArgumentDefinition(): InputValueDefinitionNode
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."
);
}

return $this->definitionNode->arguments[0];
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 !"
);
}
}

/**
Expand All @@ -138,27 +144,4 @@ abstract protected function find(string $modelClass, $idOrIds);
* @return void
*/
abstract protected function modifyExistence(Model $model): void;

/**
* Field manipulation is used to verify if usage of directive is allowed on defined field.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
*
* @return void
* @throws \Nuwave\Lighthouse\Exceptions\DirectiveException
*/
public function manipulateFieldDefinition(DocumentAST &$documentAST, FieldDefinitionNode &$fieldDefinition, ObjectTypeDefinitionNode &$parentType): void
{
if ($this->verifySoftDeletesUsed !== true) {
return;
}

if (! in_array(SoftDeletes::class, class_uses_recursive($this->getModelClass()))) {
throw new DirectiveException(
'Use @'.static::name().' directive only for Model classes that use the SoftDeletes trait!'
);
}
}
}
33 changes: 28 additions & 5 deletions src/Schema/Directives/RestoreDirective.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

namespace Nuwave\Lighthouse\Schema\Directives;

use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Illuminate\Database\Eloquent\Model;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
use Nuwave\Lighthouse\SoftDeletes\Utils;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;

class RestoreDirective extends ModifyModelExistenceDirective implements DefinedDirective
class RestoreDirective extends ModifyModelExistenceDirective implements DefinedDirective, FieldManipulator
{
protected $verifySoftDeletesUsed = true;
const MODEL_NOT_USING_SOFT_DELETES = 'Use the @restore directive only for Model classes that use the SoftDeletes trait.';

/**
* Name of the directive.
Expand Down Expand Up @@ -45,8 +50,8 @@ public static function definition(): string
/**
* Find one or more models by id.
*
* @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $modelClass
* @param string|int|string[]|int[] $idOrIds
* @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $modelClass
* @param string|int|string[]|int[] $idOrIds
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection
*/
protected function find(string $modelClass, $idOrIds)
Expand All @@ -57,11 +62,29 @@ protected function find(string $modelClass, $idOrIds)
/**
* Bring a model in or out of existence.
*
* @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\SoftDeletes $model
* @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);

Utils::assertModelUsesSoftDeletes($this->getModelClass(), self::MODEL_NOT_USING_SOFT_DELETES);
}
}
32 changes: 32 additions & 0 deletions src/SoftDeletes/Utils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Nuwave\Lighthouse\SoftDeletes;

use Illuminate\Database\Eloquent\SoftDeletes;
use Nuwave\Lighthouse\Exceptions\DefinitionException;

class Utils
{
/**
* Ensure a model uses the SoftDeletes trait.
*
* @see \Illuminate\Database\Eloquent\SoftDeletes
*
* @param string $modelClass
* @param string $exceptionMessage
* @return void
*
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
public static function assertModelUsesSoftDeletes(string $modelClass, string $exceptionMessage): void
{
if (
! in_array(
SoftDeletes::class,
class_uses_recursive($modelClass)
)
) {
throw new DefinitionException($exceptionMessage);
}
}
}
Loading