Before running a mutation, you probably want to do a few things:
ID
inputsThis guide describes how to accomplish that workflow with GraphQL-Ruby.
Before loading any data from the database, you might want to see if the user has a certain permission level. For example, maybe only .admin?
users can run Mutation.promoteEmployee
.
This check can be implemented using the #ready?
method in a mutation:
class Mutations::PromoteEmployee < Mutations::BaseMutation
def ready?(**args)
# Called with mutation args.
# Use keyword args such as employee_id: or **args to collect them
if !context[:current_user].admin?
raise GraphQL::ExecutionError, "Only admins can run this mutation"
else
# Return true to continue the mutation:
true
end
end
# ...
end
Now, when any non-admin
user tries to run the mutation, it won’t run. Instead, they’ll get an error in the response.
Additionally, #ready?
may return false, { ... }
to return errors as data:
def ready?
if !context[:current_user].allowed?
return false, { errors: ["You don't have permission to do this"]}
else
true
end
end
Often, mutations take ID
s as input and use them to load records from the database. GraphQL-Ruby can load IDs for you when you provide a loads:
option.
In short, here’s an example:
class Mutations::PromoteEmployee < Mutations::BaseMutation
# `employeeId` is an ID, Types::Employee is an _Object_ type
argument :employee_id, ID, required: true, loads: Types::Employee
# Behind the scenes, `:employee_id` is used to fetch an object from the database,
# then the object is authorized with `Employee.authorized?`, then
# if all is well, the object is injected here:
def resolve(employee:)
employee.promote!
end
end
It works like this: if you pass a loads:
option, it will:
_id
from the name and pass that name for the as:
optionID
(using Schema.object_from_id
)loads:
type (using Schema.resolve_type
).authorized?
hook (see Authorization)#resolve
using the object-style name (employee:
)In this case, if the argument value is provided by object_from_id
doesn’t return a value, the mutation will fail with an error.
If you don’t want this behavior, don’t use it. Instead, create arguments with type ID
and use them your own way, for example:
# No special loading behavior:
argument :employee_id, ID, required: true
Sometimes you need to authorize a specific user-object(s)-action combination. For example, .admin?
users can’t promote all employees! They can only promote employees which they manage.
You can add this check by implementing a #authorized?
method, for example:
def authorized?(employee:)
context[:current_user].manager_of?(employee)
end
When #authorized?
returns false
, the mutation will be halted. If it returns true
(or something truthy), the mutation will continue.
To add errors as data (as described in Mutation errors), return a value along with false
, for example:
def authorized?(employee:)
if context[:current_user].manager_of?(employee)
true
else
return false, { errors: ["Can't promote an employee you don't manage"] }
end
end
Alternatively, you can add top-level errors by raising GraphQL::ExecutionError
, for example:
def authorized?(employee:)
if !context[:current_user].manager_of?(employee)
raise GraphQL::ExecutionError, "You can only promote your _own_ employees"
end
end
In either case (returning [false, data]
or raising an error), the mutation will be halted.
Now that the user has been authorized in general, data has been loaded, and objects have been validated in particular, you can modify the database using #resolve
:
def resolve(employee:)
if employee.promote
{
employee: employee,
errors: [],
}
else
# See "Mutation Errors" for more:
{
errors: employee.errors.full_messages
}
end
end