diff --git a/src/Execution/MutationExecutor.php b/src/Execution/MutationExecutor.php new file mode 100644 index 0000000000..3e5360b96d --- /dev/null +++ b/src/Execution/MutationExecutor.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) + { + $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/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/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/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']); }); - } }