Is your feature request related to a problem? Please describe.
I've been looking at a way for my mutations to leverage some existing model methods, either through using @field or @method but so far I've come up short - but I could be approaching this all wrong!
Essentially, say I have a user model
class User extends Model
{
public function promote($levels = 1): User
{
return tap($this, function ($instance) {
$instance->level = $instance-> level + $levels;
$instance->save();
}
}
}
And I wanted to expose this via GraphQL
extend type Mutation {
promoteUser(id: ID! @call(name: "promote")): User
}
and then from my front-end
mutation promoteUser(id: 10) {
name
level
}
Additionally, the ability to add arguments would be great but this adds complexity, and my initial implementation was just to perform self-contained mutations in the backend (as opposed to query -> transform -> mutate on the frontend).
I have tried a custom resolver (with @field), but this looks like overkill and I'm not sure if I can simply extend the existing default resolvers to add my method on? Perhaps an example of this could help?
I tried @method briefly but I didn't understand the implementation.
The other I thought of, was a custom directive, but it seems odd to create a custom directive for every method I have already implemented and would end up with lots of single-use directives.
Am I approaching this wrong? Any advice?
Thanks, and I love the work you're doing!
You could make a generic custom directive, like this (you will need to modify it as your needs)
<?php
namespace App\Graphql\Directives;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class CallDirective extends BaseDirective implements FieldResolver
{
/**
* Name of the directive.
*
* @return string
*/
public function name()
{
return 'call';
}
/**
* Resolve the field directive.
*
* @param FieldValue $value
*
* @return FieldValue
* @throws DirectiveException
*/
public function resolveField(FieldValue $value)
{
$model = $this->getModel();
$method = $this->directiveArgValue('method');
return $value->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($model, $method) {
$instance = $model::findOrFail(Arr::get($args, 'input.id'));
return $instance->{$method}();
});
}
private function getModel()
{
$model = $this->directiveArgValue('model');
// Fallback to using information from the schema definition as the model name
if (! $model) {
$model = ASTHelper::getUnderlyingTypeName($this->definitionNode);
// Cut the added type suffix to get the base model class name
$model = Str::before($model, 'Collection');
$model = Str::before($model, 'Paginator');
$model = Str::before($model, 'Connection');
}
if (! $model) {
throw new DirectiveException(
"A `model` argument must be assigned to the '{$this->name()}' directive on '{$this->definitionNode->name->value}"
);
}
return $this->namespaceModelClass($model);
}
}
A generic custom directive sounds like a good way to go.
Would I still need the @update directive for the mutation?
If I do have @update, this doesn't seem to be called at all, and if I don't, I get Could not locate a default resolver for the field foo.
Thanks for your help.
You can't have two field resolver directives. You has to use it like:
input PromoteUserInput {
id: ID
}
extend type Mutation {
promoteUser(input: PromoteUserInput!): User @call(method: "promote")
}
@enzonotario great solution. Maybe we can enhance the docs with a guide for using custom directives.
Sure! It would be great but for me is so hard to write english... I will try to at least write something in spanish and then translate it by parts
@enzonotario i really appreciate the effort. I think your english is quite good, you are always doing a great job of communicating with the community in issues like these :)
Just want to say this was really helpful and gave me a really good starting point 馃憤
I was able to get this example working as a field resolver:
<?php
namespace App\Graphql\Directives;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Exceptions\DirectiveException;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
class CallDirective extends BaseDirective implements FieldResolver
{
/**
* Name of the directive.
*
* @return string
*/
public function name()
{
return 'call';
}
/**
* Resolve the field directive.
*
* @param FieldValue $value
*
* @return FieldValue
* @throws DirectiveException
*/
public function resolveField(FieldValue $value)
{
$model = $this->getModel();
$method = $this->directiveArgValue('method');
$arguments = $this->directiveArgValue('args') ?? [];
return $value->setResolver(function ($root, array $args) use ($model, $method, $arguments) {
$instance = $model::where($args)->firstOrFail();
$instance->{$method}(...$arguments);
return $instance->refresh();
});
}
/**
* Resolve the model name
*
* @return string
*/
protected function getModel()
{
$model = $this->directiveArgValue('model');
// Fallback to using information from the schema definition as the model name
if (! $model) {
$model = ASTHelper::getUnderlyingTypeName($this->definitionNode);
// Cut the added type suffix to get the base model class name
$model = Str::before($model, 'Collection');
$model = Str::before($model, 'Paginator');
$model = Str::before($model, 'Connection');
}
if (! $model) {
throw new DirectiveException(
"A `model` argument must be assigned to the '{$this->name()}' directive on '{$this->definitionNode->name->value}"
);
}
return $this->namespaceModelClass($model);
}
}
This will find the first model, call the provided method on that model instance and return a freshly resolved model in the response:
type Mutation {
promoteUser(id: ID! @eq): User @call(name: "promote")
}
It'll work with any number of input params as well:
type Mutation {
promoteUser(
id: ID @eq
username: String @eq
): User @call(name: "promote")
}
You can even set static arguments that should passed to the method:
type Mutation {
promoteUser(id: ID! @eq): User @call(name: "setStatus", args: ["promoted"])
}
Thanks @enzonotario for the great work!
Most helpful comment
@enzonotario i really appreciate the effort. I think your english is quite good, you are always doing a great job of communicating with the community in issues like these :)