From cc795d53e7c758ca33932447edf70bd242bb2c55 Mon Sep 17 00:00:00 2001 From: spawnia Date: Tue, 17 Jul 2018 16:14:18 +0200 Subject: [PATCH 1/2] Add basic nested mutations --- src/Execution/NestedMutationExecutor.php | 101 ++++++++++++++++++ .../Fields/CreateNestedDirective.php | 40 +++++++ .../Fields/UpdateNestedDirective.php | 40 +++++++ 3 files changed, 181 insertions(+) create mode 100644 src/Execution/NestedMutationExecutor.php create mode 100644 src/Schema/Directives/Fields/CreateNestedDirective.php create mode 100644 src/Schema/Directives/Fields/UpdateNestedDirective.php diff --git a/src/Execution/NestedMutationExecutor.php b/src/Execution/NestedMutationExecutor.php new file mode 100644 index 0000000000..9a0ee76044 --- /dev/null +++ b/src/Execution/NestedMutationExecutor.php @@ -0,0 +1,101 @@ +fill($remaining->all()); + + $belongsTo->each(function ($value, $key) use ($model) { + $model->{$key}()->associate($value); + }); + + $parentRelation + ? $parentRelation->save($model) + : $model->save(); + + $hasMany->each(function ($nestedOperations, $key) use ($model) { + /** @var HasMany $relation */ + $relation = $model->{$key}(); + + collect($nestedOperations)->each(function ($values, $operationKey) use ($relation) { + if ($operationKey === 'create') { + self::handleHasManyCreate(collect($values), $relation); + } + }); + }); + + return $model; + } + + protected static function handleHasManyCreate(Collection $multiValues, HasMany $relation): void + { + $multiValues->each(function ($singleValues) use ($relation) { + self::executeCreate($relation->getModel()->newInstance(), collect($singleValues), $relation); + }); + } + + public static function executeUpdate(Model $model, Collection $args, ?HasMany $parentRelation = null): Model + { + list($belongsTo, $remaining) = self::extractBelongsToArgs($model, $args); + list($hasMany, $remaining) = self::extractHasManyArgs($model, $remaining); + + $model = $model->newQuery()->findOrFail($args->pull('id')); + $model->fill($remaining->all()); + + $belongsTo->each(function ($value, $key) use ($model) { + $model->{$key}()->associate($value); + }); + + $parentRelation + ? $parentRelation->save($model) + : $model->save(); + + $hasMany->each(function ($nestedOperations, $key) use ($model) { + /** @var HasMany $relation */ + $relation = $model->{$key}(); + + collect($nestedOperations)->each(function ($values, $operationKey) use ($relation) { + if ($operationKey === 'create') { + self::handleHasManyCreate(collect($values), $relation); + } + + if ($operationKey === 'update') { + collect($values)->each(function ($singleValues) use ($relation) { + self::executeUpdate($relation->getModel()->newInstance(), collect($singleValues), $relation); + }); + } + + if ($operationKey === 'delete') { + $relation->getModel()::destroy($values); + } + }); + }); + + return $model; + } + + protected static function extractBelongsToArgs(Model $model, Collection $args): Collection + { + return $args->partition(function ($value, $key) use ($model) { + return method_exists($model, $key) && ($model->{$key}() instanceof BelongsTo); + }); + } + + protected static function extractHasManyArgs(Model $model, Collection $args): Collection + { + return $args->partition(function ($value, $key) use ($model) { + return method_exists($model, $key) && ($model->{$key}() instanceof HasMany); + }); + } +} diff --git a/src/Schema/Directives/Fields/CreateNestedDirective.php b/src/Schema/Directives/Fields/CreateNestedDirective.php new file mode 100644 index 0000000000..5264c46036 --- /dev/null +++ b/src/Schema/Directives/Fields/CreateNestedDirective.php @@ -0,0 +1,40 @@ +getModelClass(); + $model = new $modelClassName(); + + return $value->setResolver(function ($root, $args) use ($model) { + return NestedMutationExecutor::executeCreate($model, collect($args['input'])); + }); + } +} diff --git a/src/Schema/Directives/Fields/UpdateNestedDirective.php b/src/Schema/Directives/Fields/UpdateNestedDirective.php new file mode 100644 index 0000000000..6ab5e18504 --- /dev/null +++ b/src/Schema/Directives/Fields/UpdateNestedDirective.php @@ -0,0 +1,40 @@ +getModelClass(); + $model = new $modelClassName(); + + return $value->setResolver(function ($root, $args) use ($model) { + return NestedMutationExecutor::executeUpdate($model, collect($args['input'])); + }); + } +} From 7198183d5776a009a853e165d2e41b6bee488703 Mon Sep 17 00:00:00 2001 From: spawnia Date: Sat, 28 Jul 2018 00:42:21 +0200 Subject: [PATCH 2/2] Integrate nested mutations with the previous @create and @update directives - Add tests for basic functionality of create and update - Add flag "flatten" to enable use with a single input object - Fully define all relationships on test models - Get rid of PHP 7.1 annotations --- ...ationExecutor.php => MutationExecutor.php} | 12 +- .../Directives/Fields/CreateDirective.php | 14 +- .../Fields/CreateNestedDirective.php | 40 ----- .../Directives/Fields/UpdateDirective.php | 52 ++---- .../Fields/UpdateNestedDirective.php | 40 ----- .../Directives/Fields/CreateDirectiveTest.php | 124 +++++++++++++++ .../Directives/Fields/UpdateDirectiveTest.php | 149 ++++++++++++++++++ tests/Utils/Models/Comment.php | 15 +- tests/Utils/Models/Company.php | 8 + tests/Utils/Models/Post.php | 12 +- tests/Utils/Models/Task.php | 5 +- tests/Utils/Models/Team.php | 7 + tests/Utils/Models/User.php | 21 ++- 13 files changed, 348 insertions(+), 151 deletions(-) rename src/Execution/{NestedMutationExecutor.php => MutationExecutor.php} (95%) delete mode 100644 src/Schema/Directives/Fields/CreateNestedDirective.php delete mode 100644 src/Schema/Directives/Fields/UpdateNestedDirective.php create mode 100644 tests/Integration/Schema/Directives/Fields/CreateDirectiveTest.php create mode 100644 tests/Integration/Schema/Directives/Fields/UpdateDirectiveTest.php diff --git a/src/Execution/NestedMutationExecutor.php b/src/Execution/MutationExecutor.php similarity index 95% rename from src/Execution/NestedMutationExecutor.php rename to src/Execution/MutationExecutor.php index 9a0ee76044..3e5360b96d 100644 --- a/src/Execution/NestedMutationExecutor.php +++ b/src/Execution/MutationExecutor.php @@ -7,15 +7,15 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; -class NestedMutationExecutor +class MutationExecutor { - public static function executeCreate(Model $model, Collection $args, ?HasMany $parentRelation = null): Model + public static function executeCreate(Model $model, Collection $args, HasMany $parentRelation = null): Model { list($belongsTo, $remaining) = self::extractBelongsToArgs($model, $args); list($hasMany, $remaining) = self::extractHasManyArgs($model, $remaining); - + $model->fill($remaining->all()); - + $belongsTo->each(function ($value, $key) use ($model) { $model->{$key}()->associate($value); }); @@ -38,14 +38,14 @@ public static function executeCreate(Model $model, Collection $args, ?HasMany $p return $model; } - protected static function handleHasManyCreate(Collection $multiValues, HasMany $relation): void + protected static function handleHasManyCreate(Collection $multiValues, HasMany $relation) { $multiValues->each(function ($singleValues) use ($relation) { self::executeCreate($relation->getModel()->newInstance(), collect($singleValues), $relation); }); } - public static function executeUpdate(Model $model, Collection $args, ?HasMany $parentRelation = null): Model + public static function executeUpdate(Model $model, Collection $args, HasMany $parentRelation = null): Model { list($belongsTo, $remaining) = self::extractBelongsToArgs($model, $args); list($hasMany, $remaining) = self::extractHasManyArgs($model, $remaining); diff --git a/src/Schema/Directives/Fields/CreateDirective.php b/src/Schema/Directives/Fields/CreateDirective.php index 9954db8a5f..1ea052a25c 100644 --- a/src/Schema/Directives/Fields/CreateDirective.php +++ b/src/Schema/Directives/Fields/CreateDirective.php @@ -2,10 +2,10 @@ namespace Nuwave\Lighthouse\Schema\Directives\Fields; -use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Execution\MutationExecutor; +use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; -use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; class CreateDirective extends BaseDirective implements FieldResolver { @@ -28,8 +28,14 @@ public function name() */ public function resolveField(FieldValue $value) { - return $value->setResolver(function ($root, array $args){ - return $this->getModelClass()::create($args); + return $value->setResolver(function ($root, $args) { + $modelClassName = $this->getModelClass(); + $model = new $modelClassName(); + + $flatten = $this->directiveArgValue('flatten', false); + $args = $flatten ? reset($args) : $args; + + return MutationExecutor::executeCreate($model, collect($args)); }); } } diff --git a/src/Schema/Directives/Fields/CreateNestedDirective.php b/src/Schema/Directives/Fields/CreateNestedDirective.php deleted file mode 100644 index 5264c46036..0000000000 --- a/src/Schema/Directives/Fields/CreateNestedDirective.php +++ /dev/null @@ -1,40 +0,0 @@ -getModelClass(); - $model = new $modelClassName(); - - return $value->setResolver(function ($root, $args) use ($model) { - return NestedMutationExecutor::executeCreate($model, collect($args['input'])); - }); - } -} diff --git a/src/Schema/Directives/Fields/UpdateDirective.php b/src/Schema/Directives/Fields/UpdateDirective.php index 4d5886e989..f64c215dc9 100644 --- a/src/Schema/Directives/Fields/UpdateDirective.php +++ b/src/Schema/Directives/Fields/UpdateDirective.php @@ -2,13 +2,12 @@ namespace Nuwave\Lighthouse\Schema\Directives\Fields; -use GraphQL\Type\Definition\IDType; -use Nuwave\Lighthouse\Schema\Directives\BaseDirective; -use Nuwave\Lighthouse\Schema\Resolvers\NodeResolver; use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Execution\MutationExecutor; +use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId; +use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Exceptions\DirectiveException; -use Nuwave\Lighthouse\Support\Traits\HandlesGlobalId; class UpdateDirective extends BaseDirective implements FieldResolver { @@ -34,47 +33,18 @@ public function name() */ public function resolveField(FieldValue $value) { - $idArg = $this->getIDField($value); - $globalId = $this->directiveArgValue('globalId', false); + return $value->setResolver(function ($root, $args) { + $modelClassName = $this->getModelClass(); + $model = new $modelClassName(); - if (!$idArg) { - new DirectiveException(sprintf( - 'The `update` requires that you have an `ID` field on %s', - $value->getNodeName() - )); - } + $flatten = $this->directiveArgValue('flatten', false); + $args = $flatten ? reset($args) : $args; - return $value->setResolver(function ($root, array $args) use ($idArg, $globalId) { - $id = $globalId ? $this->decodeGlobalId(array_get($args, $idArg))[1] : array_get($args, $idArg); - - $model = $this->getModelClass()::find($id); - - if ($model) { - $attributes = collect($args)->except([$idArg])->toArray(); - $model->fill($attributes); - $model->save(); + if($this->directiveArgValue('globalId', false)){ + $args['id'] = $this->decodeGlobalId($args['id'])[1]; } - return $model; + return MutationExecutor::executeUpdate($model, collect($args)); }); } - - /** - * Check if field has an ID argument. - * - * @param FieldValue $value - * - * @return bool - */ - protected function getIDField(FieldValue $value) - { - return collect($value->getField()->arguments)->filter(function ($arg) { - $type = NodeResolver::resolve($arg->type); - $type = method_exists($type, 'getWrappedType') ? $type->getWrappedType() : $type; - - return $type instanceof IDType; - })->map(function ($arg) { - return $arg->name->value; - })->first(); - } } diff --git a/src/Schema/Directives/Fields/UpdateNestedDirective.php b/src/Schema/Directives/Fields/UpdateNestedDirective.php deleted file mode 100644 index 6ab5e18504..0000000000 --- a/src/Schema/Directives/Fields/UpdateNestedDirective.php +++ /dev/null @@ -1,40 +0,0 @@ -getModelClass(); - $model = new $modelClassName(); - - return $value->setResolver(function ($root, $args) use ($model) { - return NestedMutationExecutor::executeUpdate($model, collect($args['input'])); - }); - } -} diff --git a/tests/Integration/Schema/Directives/Fields/CreateDirectiveTest.php b/tests/Integration/Schema/Directives/Fields/CreateDirectiveTest.php new file mode 100644 index 0000000000..adf8f75036 --- /dev/null +++ b/tests/Integration/Schema/Directives/Fields/CreateDirectiveTest.php @@ -0,0 +1,124 @@ +execute($schema, $query); + + $this->assertSame('1', array_get($result, 'data.createCompany.id')); + $this->assertSame('foo', array_get($result, 'data.createCompany.name')); + } + + /** + * @test + */ + public function itCanCreateFromInputObject() + { + $schema = ' + type Company { + id: ID! + name: String! + } + + type Mutation { + createCompany(input: CreateCompanyInput!): Company @create(flatten: true) + } + + input CreateCompanyInput { + name: String + } + '; + $query = ' + mutation { + createCompany(input: { + name: "foo" + }) { + id + name + } + } + '; + $result = $this->execute($schema, $query); + + $this->assertSame('1', array_get($result, 'data.createCompany.id')); + $this->assertSame('foo', array_get($result, 'data.createCompany.name')); + } + + /** + * @test + */ + public function itCanCreateWithBelongsTo() + { + factory(User::class)->create(); + + $schema = ' + type Task { + id: ID! + name: String! + user: User @belongsTo + } + + type User { + id: ID + } + + type Mutation { + createTask(input: CreateTaskInput!): Task @create(flatten: true) + } + + input CreateTaskInput { + name: String + user: ID + } + '; + $query = ' + mutation { + createTask(input: { + name: "foo" + user: 1 + }) { + id + name + user { + id + } + } + } + '; + $result = $this->execute($schema, $query); + + $this->assertSame('1', array_get($result, 'data.createTask.id')); + $this->assertSame('foo', array_get($result, 'data.createTask.name')); + $this->assertSame('1', array_get($result, 'data.createTask.user.id')); + } +} diff --git a/tests/Integration/Schema/Directives/Fields/UpdateDirectiveTest.php b/tests/Integration/Schema/Directives/Fields/UpdateDirectiveTest.php new file mode 100644 index 0000000000..f49e46b2cb --- /dev/null +++ b/tests/Integration/Schema/Directives/Fields/UpdateDirectiveTest.php @@ -0,0 +1,149 @@ +create(['name' => 'foo']); + $schema = ' + type Company { + id: ID! + name: String! + } + + type Mutation { + updateCompany( + id: ID! + name: String + ): Company @update + } + '; + $query = ' + mutation { + updateCompany( + id: 1 + name: "bar" + ) { + id + name + } + } + '; + $result = $this->execute($schema, $query); + + $this->assertSame('1', array_get($result, 'data.updateCompany.id')); + $this->assertSame('bar', array_get($result, 'data.updateCompany.name')); + $this->assertSame('bar', Company::first()->name); + } + /** + * @test + */ + public function itCanUpdateFromInputObject() + { + factory(Company::class)->create(['name' => 'foo']); + $schema = ' + type Company { + id: ID! + name: String! + } + + type Mutation { + updateCompany( + input: UpdateCompanyInput + ): Company @update(flatten: true) + } + + input UpdateCompanyInput { + id: ID! + name: String + } + '; + $query = ' + mutation { + updateCompany(input: { + id: 1 + name: "bar" + }) { + id + name + } + } + '; + $result = $this->execute($schema, $query); + + $this->assertSame('1', array_get($result, 'data.updateCompany.id')); + $this->assertSame('bar', array_get($result, 'data.updateCompany.name')); + $this->assertSame('bar', Company::first()->name); + } + + /** + * @test + */ + public function itCanUpdateWithBelongsTo() + { + factory(User::class, 2)->create(); + factory(Task::class)->create([ + 'name' => 'bar', + 'user_id' => 1, + ]); + + $schema = ' + type Task { + id: ID! + name: String! + user: User @belongsTo + } + + type User { + id: ID + } + + type Mutation { + updateTask(input: UpdateTaskInput!): Task @update(flatten: true) + } + + input UpdateTaskInput { + id: ID! + name: String + user: ID + } + '; + $query = ' + mutation { + updateTask(input: { + id: 1 + name: "foo" + user: 2 + }) { + id + name + user { + id + } + } + } + '; + $result = $this->execute($schema, $query); + + $this->assertSame('1', array_get($result, 'data.updateTask.id')); + $this->assertSame('foo', array_get($result, 'data.updateTask.name')); + $this->assertSame('2', array_get($result, 'data.updateTask.user.id')); + + $task = Task::first(); + $this->assertSame(2, $task->user_id); + $this->assertSame('foo', $task->name); + } +} diff --git a/tests/Utils/Models/Comment.php b/tests/Utils/Models/Comment.php index 4b870968c6..534194652e 100644 --- a/tests/Utils/Models/Comment.php +++ b/tests/Utils/Models/Comment.php @@ -1,12 +1,21 @@ belongsTo(User::class); + } -} \ No newline at end of file + public function post(): BelongsTo + { + return $this->belongsTo(Post::class); + } +} diff --git a/tests/Utils/Models/Company.php b/tests/Utils/Models/Company.php index 262c1b1396..f65da12c80 100644 --- a/tests/Utils/Models/Company.php +++ b/tests/Utils/Models/Company.php @@ -3,9 +3,17 @@ namespace Tests\Utils\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Nuwave\Lighthouse\Support\Traits\IsRelayConnection; class Company extends Model { use IsRelayConnection; + + protected $guarded = []; + + public function users(): HasMany + { + return $this->hasMany(User::class); + } } diff --git a/tests/Utils/Models/Post.php b/tests/Utils/Models/Post.php index 568cab531c..5dbb16c2b0 100644 --- a/tests/Utils/Models/Post.php +++ b/tests/Utils/Models/Post.php @@ -1,17 +1,19 @@ hasMany(Comment::class); } -} \ No newline at end of file +} diff --git a/tests/Utils/Models/Task.php b/tests/Utils/Models/Task.php index 9e1b80d58f..07492e9411 100644 --- a/tests/Utils/Models/Task.php +++ b/tests/Utils/Models/Task.php @@ -4,13 +4,16 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Nuwave\Lighthouse\Support\Traits\IsRelayConnection; class Task extends Model { use IsRelayConnection, SoftDeletes; - public function user() + protected $guarded = []; + + public function user(): BelongsTo { return $this->belongsTo(User::class); } diff --git a/tests/Utils/Models/Team.php b/tests/Utils/Models/Team.php index 03f4ea3eea..2a6ee235b2 100644 --- a/tests/Utils/Models/Team.php +++ b/tests/Utils/Models/Team.php @@ -3,7 +3,14 @@ namespace Tests\Utils\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; class Team extends Model { + protected $guarded = []; + + public function users(): HasMany + { + return $this->hasMany(User::class); + } } diff --git a/tests/Utils/Models/User.php b/tests/Utils/Models/User.php index 38ab1f51ce..ee5522307a 100644 --- a/tests/Utils/Models/User.php +++ b/tests/Utils/Models/User.php @@ -2,6 +2,9 @@ namespace Tests\Utils\Models; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Nuwave\Lighthouse\Support\Traits\IsRelayConnection; @@ -9,36 +12,32 @@ class User extends Authenticatable { use IsRelayConnection; - public function company() + protected $guarded = []; + + public function company(): BelongsTo { return $this->belongsTo(Company::class); } - public function team() + public function team(): BelongsTo { return $this->belongsTo(Team::class); } - public function tasks() + public function tasks(): HasMany { return $this->hasMany(Task::class); } - public function posts() + public function posts(): HasMany { return $this->hasMany(Post::class); } - /** - * @param $query User - * @param $args - * @return mixed - */ - public function scopeCompanyName($query, $args) + public function scopeCompanyName(Builder $query, array $args): Builder { return $query->whereHas("company", function($q) use ($args){ $q->where("name", $args['company']); }); - } }