Skip to content
2 changes: 1 addition & 1 deletion src/Error/ValidationError.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ValidationError extends Error {
* setValidator
*
* @param Validator $validator
* @return void
* @return self
*/
public function setValidator(Validator $validator) {
$this->validator = $validator;
Expand Down
2 changes: 0 additions & 2 deletions src/GraphQLController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class GraphQLController extends Controller {
* @return \Illuminate\Http\JsonResponse
*/
public function query(Request $request, $schema = null) {

$inputs = $request->all();
$data = [];

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AbstractRelationTransformer {
/**
* Values used to hydrate relation.
*
* @var array
* @var array|null
*/
protected $values;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -79,6 +79,6 @@ protected function associate() {
/**
* Meant to be executed after Relation owner save.
*/
public function afterSAve() {
public function afterSave() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace StudioNet\GraphQL\Support\Transformer\Eloquent\Relation;

use GraphQL\Error\Error;

class MorphToManyRelationTransformer extends AbstractRelationTransformer {

/**
* Store values in entity.
*/
protected function hydrate() {
// if values is null or empty array, break execution of hydrate. Syncing is done in afterSave!
if (is_null($this->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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down
18 changes: 11 additions & 7 deletions src/Support/Transformer/Eloquent/StoreTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions tests/Definition/UserDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
}
Expand Down
3 changes: 3 additions & 0 deletions tests/Entity/Label.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
164 changes: 164 additions & 0 deletions tests/GraphQLMutationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down