diff --git a/src/Error/ValidationError.php b/src/Error/ValidationError.php index 8d38cde..24f9a0a 100644 --- a/src/Error/ValidationError.php +++ b/src/Error/ValidationError.php @@ -17,7 +17,7 @@ class ValidationError extends Error { * setValidator * * @param Validator $validator - * @return void + * @return self */ public function setValidator(Validator $validator) { $this->validator = $validator; diff --git a/src/GraphQLController.php b/src/GraphQLController.php index 4a28a6c..7375c1b 100644 --- a/src/GraphQLController.php +++ b/src/GraphQLController.php @@ -22,7 +22,6 @@ class GraphQLController extends Controller { * @return \Illuminate\Http\JsonResponse */ public function query(Request $request, $schema = null) { - $inputs = $request->all(); $data = []; @@ -48,7 +47,6 @@ public function query(Request $request, $schema = null) { else { $data = $this->executeQuery($schema, $inputs); } - } catch (\Exception $exception) { $data = GraphQL::formatGraphQLException($exception); Log::debug($exception); diff --git a/src/Support/Transformer/Eloquent/Relation/AbstractRelationTransformer.php b/src/Support/Transformer/Eloquent/Relation/AbstractRelationTransformer.php index 20eb475..0e3a981 100644 --- a/src/Support/Transformer/Eloquent/Relation/AbstractRelationTransformer.php +++ b/src/Support/Transformer/Eloquent/Relation/AbstractRelationTransformer.php @@ -17,7 +17,7 @@ class AbstractRelationTransformer { /** * Values used to hydrate relation. * - * @var array + * @var array|null */ protected $values; @@ -45,7 +45,7 @@ class AbstractRelationTransformer { /** * Constructor. */ - public function __construct(Model $model, string $column, array $values) { + public function __construct(Model $model, string $column, ?array $values) { $this->model = $model; $this->column = $column; $this->values = $values; @@ -79,6 +79,6 @@ protected function associate() { /** * Meant to be executed after Relation owner save. */ - public function afterSAve() { + public function afterSave() { } } diff --git a/src/Support/Transformer/Eloquent/Relation/MorphToManyRelationTransformer.php b/src/Support/Transformer/Eloquent/Relation/MorphToManyRelationTransformer.php new file mode 100644 index 0000000..c8739d9 --- /dev/null +++ b/src/Support/Transformer/Eloquent/Relation/MorphToManyRelationTransformer.php @@ -0,0 +1,68 @@ +values) || count($this->values) === 0) { + return; + } + + // todo: this is nice workaround, but it would be better to define good graphql type instead! + if (!is_array(array_first($this->values))) { + $this->values = [$this->values]; + } + + $relatedModelClassName = '\\' . get_class($this->relation->getRelated()); + + // collect IDs from related objects + $relationIDs = []; + + // create new related items, if id is empty, otherwise update related item. then collect IDs + foreach ($this->values as $values) { + // check, if given attributes are fillable + $valuesExceptId = array_filter($values, function ($key) { + return $key !== 'id'; + }, ARRAY_FILTER_USE_KEY); + $relatedModelEmptyObject = $this->relation->newModelInstance(); + foreach (array_keys($valuesExceptId) as $key) { + if (!$relatedModelEmptyObject->isFillable($key)) { + throw new Error("Attribute [{$key}] on Model {$relatedModelClassName} is not fillable!"); + } + } + // create new related object + if (empty(array_get($values, 'id', null))) { + // check, if given values are fillable + $newRelatedItem = $this->relation->create($values); + array_push($relationIDs, $newRelatedItem->id); + } else { + // update related object, of there are more then one keys in values array + if (count($values) > 1) { + // if there are more then one element in $values, update related item + // use fully qualified name, to get related object + // because if parent object has no relation with given related object, $relation->find() will return null... + $relatedItem = $relatedModelClassName::findOrFail($values['id']); + $relatedItem->fill($valuesExceptId)->save(); + } + array_push($relationIDs, $values['id']); + } + } + + // overwrite $values with IDs, so they are available in afterSave() method + $this->values = $relationIDs; + } + + /** + * Override. + */ + public function afterSave() { + $this->relation->sync($this->values); + } +} diff --git a/src/Support/Transformer/Eloquent/Relation/RelationTransformerFactory.php b/src/Support/Transformer/Eloquent/Relation/RelationTransformerFactory.php index 3ad66ee..94c4daa 100644 --- a/src/Support/Transformer/Eloquent/Relation/RelationTransformerFactory.php +++ b/src/Support/Transformer/Eloquent/Relation/RelationTransformerFactory.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Model; class RelationTransformerFactory { - public static function getTransformer(Model $model, string $column, array $values) { + public static function getTransformer(Model $model, string $column, ?array $values) { $relation = $model->{$column}(); $classesToTest = []; diff --git a/src/Support/Transformer/Eloquent/StoreTransformer.php b/src/Support/Transformer/Eloquent/StoreTransformer.php index a519400..06fc6c6 100644 --- a/src/Support/Transformer/Eloquent/StoreTransformer.php +++ b/src/Support/Transformer/Eloquent/StoreTransformer.php @@ -2,11 +2,11 @@ namespace StudioNet\GraphQL\Support\Transformer\Eloquent; use Illuminate\Database\Eloquent\Builder; +use StudioNet\GraphQL\Support\Transformer\Eloquent\Relation\MorphToManyRelationTransformer; use StudioNet\GraphQL\Support\Transformer\EloquentTransformer; use StudioNet\GraphQL\Support\Transformer\Eloquent\Relation\RelationTransformer; use StudioNet\GraphQL\Support\Definition\Definition; use StudioNet\GraphQL\Definition\Type; -use Illuminate\Database\Eloquent\Relations; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Validator; use StudioNet\GraphQL\Error\ValidationError; @@ -68,6 +68,7 @@ public function getArguments(Definition $definition) { * @param array $data * @param array $rules * @return void + * @throws ValidationError */ protected function validate(array $data, array $rules) { $validator = Validator::make($data, $rules); @@ -130,17 +131,20 @@ protected function getResolver(array $opts) { $model->fill($data); $relationTransformers = []; foreach ($relationInput as $column => $values) { - if (empty($values)) { - // TODO: check if it's pertinent - // empty values are ignored because, currently, nothing is deleted through nested update - // it can be problematic because empty top level fields are emptied. - continue; - } $relationTransformer = Relation\RelationTransformerFactory::getTransformer( $model, $column, $values ); + // earlier, all empty $values were ignored. + // But morph manyToMany relation transformer can handle it. + //May be other relation transformers can do it later too, then they has to be whitelisted here + if (MorphToManyRelationTransformer::class !== get_class($relationTransformer) && empty($values)) { + // TODO: check if it's pertinent + // empty values are ignored because, currently, nothing is deleted through nested update + // it can be problematic because empty top level fields are emptied. + continue; + } $relationTransformer->transform(); $relationTransformers[] = $relationTransformer; } diff --git a/tests/Definition/UserDefinition.php b/tests/Definition/UserDefinition.php index 81220c8..64ce3ef 100644 --- a/tests/Definition/UserDefinition.php +++ b/tests/Definition/UserDefinition.php @@ -73,6 +73,7 @@ public function getMutable() { 'permissions' => Type::json(), 'password' => Type::string(), 'posts' => Type::listOf(\GraphQL::input('post')), + 'labels' => Type::listOf(\GraphQL::input('label')), 'name_uppercase' => Type::string(), ]; } diff --git a/tests/Entity/Label.php b/tests/Entity/Label.php index 02bafcd..61799b2 100644 --- a/tests/Entity/Label.php +++ b/tests/Entity/Label.php @@ -8,6 +8,9 @@ class Label extends Model { public $timestamps = false; + /** @var array $fillable */ + protected $fillable = ['name']; + public function posts() { return $this->morphedByMany(Post::class, 'labelable'); } diff --git a/tests/GraphQLMutationTest.php b/tests/GraphQLMutationTest.php index f4e99a6..adc55bb 100644 --- a/tests/GraphQLMutationTest.php +++ b/tests/GraphQLMutationTest.php @@ -359,6 +359,170 @@ function () use ($post, $tagsToRetrieve, $tagsUpdate) { ); } + /** + * Test nested morph many2many mutation - create + * + * @return void + */ + public function testMorphManyToManyCreateMutation() { + factory(Entity\User::class, 1)->create(); + + $user = Entity\User::first(); + $this->registerAllDefinitions(); + + $this->specify( + 'tests morph m:n create mutation', + function () use ($user) { + $query = <<<"GQL" +mutation MutatePost { + user(id: {$user->id}, with: { labels: [{name:"Label 1"}, {name:"Label 2"}] }) { + id + labels { + name + } + } +} +GQL; + $this->assertGraphQLEquals($query, [ + 'data' => [ + 'user' => [ + 'id' => (string) $user->id, + 'labels' => [ + ["name" => "Label 1"], + ["name" => "Label 2"] + ] + ] + ] + ]); + } + ); + } + + /** + * Test nested morph many2many mutation - update + * + * @return void + */ + public function testMorphManyToManyUpdateMutation() { + $createdUsers = factory(Entity\User::class, 1)->create()->each(function ($user) { + $user->labels()->save(factory(Entity\Label::class)->make()); + $user->labels()->save(factory(Entity\Label::class)->make()); + }); + + $user = $createdUsers[0]; + $labels = $user->labels; + + $this->registerAllDefinitions(); + + $this->specify( + 'tests morph m:n update mutation', + function () use ($user, $labels) { + $query = <<<"GQL" +mutation MutatePost { + user(id: {$user->id}, with: { labels: [{id: "{$labels[0]->id}", name:"Label 1"}, {id: "{$labels[1]->id}", name: "Label 2"}] }) { + id + labels { + id + name + } + } +} +GQL; + $this->assertGraphQLEquals($query, [ + 'data' => [ + 'user' => [ + 'id' => (string) $user->id, + 'labels' => [ + [ + "id" => (string) $labels[0]->id, + "name" => "Label 1" + ], + [ + "id" => (string) $labels[1]->id, + "name" => "Label 2" + ] + ] + ] + ] + ]); + } + ); + } + + /** + * Test nested morph many2many mutation - clear/delete connection + * + * @return void + */ + public function testMorphManyToManyClearMutation() { + $createdUsers = factory(Entity\User::class, 1)->create()->each(function ($user) { + $user->labels()->save(factory(Entity\Label::class)->make()); + $user->labels()->save(factory(Entity\Label::class)->make()); + }); + + $user = $createdUsers[0]; + + $this->registerAllDefinitions(); + + $this->specify( + 'tests morph m:n update mutation', + function () use ($user) { + $query = <<<"GQL" +mutation MutatePost { + user(id: {$user->id}, with: { labels: [] }) { + id + labels { + id + name + } + } +} +GQL; + $this->assertGraphQLEquals($query, [ + 'data' => [ + 'user' => [ + 'id' => (string) $user->id, + 'labels' => [] + ] + ] + ]); + } + ); + + // test null value + $createdUsers2 = factory(Entity\User::class, 1)->create()->each(function ($user) { + $user->labels()->save(factory(Entity\Label::class)->make()); + $user->labels()->save(factory(Entity\Label::class)->make()); + }); + $user2 = $createdUsers2[0]; + + $this->specify( + 'tests morph m:n update mutation', + function () use ($user2) { + $query = <<<"GQL" +mutation MutatePost { + user(id: {$user2->id}, with: { labels: null }) { + id + labels { + id + name + } + } +} +GQL; + $this->assertGraphQLEquals($query, [ + 'data' => [ + 'user' => [ + 'id' => (string) $user2->id, + 'labels' => [] + ] + ] + ]); + } + ); + } + + /** * Test mutation with custom input field *