Hey @rmosolgo,
I saw that you added Enum, Union and other types into the library. Can it be used for polymorphic associations? For example: A Post model and Comment model both can be voted using belongs_to :votable polymorphic association.
In normal situation, we can use Rails helpers to cleanly address this, but how can this be achieved with Relay mutations. In mutations, we need to return the fields (Post or Comment) connection when a mutation occurs. Currently, I have to create two mutations to achieve this: PostVoteMutation and CommentVoteMutation to return type: return_field :post, PostType and return_field :comment, CommentType
Is it possible to use the new types you introduced that will help in reducing code duplication?
This is one of those things I've also been thinking about. We have a lot of polymorphic relations in our data model. Currently, I only use a single mutation in our graphql server when creating the related child object, but with multiple optional return types. Something like:
VoteMutation = GraphQL::Relay::Mutation.define do
...
return_field :post, PostType
return_field :comment, CommentType
...
resolve -> (inputs, ctx) {
obj = NodeIdentification.object_from_id(inputs[:id], ctx)
obj.votes.create!(...)
{ :"#{obj.class.to_s.downcase}" => obj }
}
end
But I haven't really figured how to do it in a good way in the relay client. As of now, I have defined multiple Relay.Mutations for each possible related parent object. Perhaps using class inheritance would solve this in a better way but I haven't digged any deeper into it yet.
Yes, actually this behavior has been here all along! I just pushed docs for the last part of the puzzle, the resolve_type proc.
The Interface or Union must be able to determine the GraphQL type to use for a given object. You can pass a resolve_type proc or use the default definition.
Then, you can use polymorphic types as described in the spec. For example, given
field :searchMedia, types[SearchResultUnion]
you could query:
searchMedia(name: "abc") {
... on Photo {
height
width
}
... on Video {
height
width
duration
}
... on Audio {
duration
key
}
}
@rmosolgo Awesome! thank you :+1:
Hey @kristianm thanks for pointing out :+1: I kind of came onto same conclusion until Robert confirmed docs for new types that could be used instead - resolve_type looks much better.
For client side, you could use inline props while invoking mutation to send type to mutation and perform mutation query accordingly. Here is vote mutation class for your reference (needs refactoring): https://github.com/gauravtiwari/relay-rails-blog/blob/master/client/app/bundles/Mutations/VoteMutations.js
Thanks @gauravtiwari for your samples (and for your blog, was very useful to me when becoming introduced to graphql/relay with rails). I'm not so fond of those if .. else if .. setups when dealing with polymorphic logic, but it currently seems like the only way. I've tried to declare a superclass which extends Relay.Mutation and contains all shared code, and then inherit from this class for all polymorphic types which often just declare the type name. Unfortunately relay does a lot of js magic which seems to break this kind of setup.
Wouldn't this work where you declare a Votable GraphQL Type and have the Post and Comment implement that interface? Then have the properties related to voting on the Votable type and have the Mutations work on the Votable type? I'm looking to do something similar and that is my plan for the approach, will that not work?
@rmosolgo, could you possibly give an example on how polymorphism in mutations would be implemented on the server? For example an upvoteMutation that can be used for both a Post and Comment?
@mathiasjakobsen Did you ever find out how to implement such mutation?
@fullofcaffeine, no I did not.
After having used this library for a bit, the way I would implement this is not directly through GraphQL type checking. But the "upvoteMutation" should accept a generic Node ID (which is essentially just an encoded Classname and ID) then you test the object returned to see what class it is and if it's in the allowed list. If not, you throw a GraphQL error so clients get the notification. But if you're in control of the clients, you should never call it on anything other than a Post or Comment.
I have a simple loader
module GraphLoader
def self.can_create(shouldBeType, ctx)
user = ctx[:current_user] || User.new
raise Exceptions::GraphExecutionError.new('User does not have permission to perform operation') unless user.can?(:create, shouldBeType)
end
def self.load(objectId, shouldBeType, ctx, permission, must_exist = true)
user = ctx[:current_user] || User.new
obj = GraphSchema.object_from_id(objectId, ctx)
if must_exist && obj.nil?
raise Exceptions::GraphExecutionError.new('Object does not exist')
end
if obj.present?
raise Exceptions::GraphExecutionError.new('ID did not return the proper type') unless obj.class == shouldBeType
raise Exceptions::GraphExecutionError.new('User does not have permission to perform operation') unless user.can?(permission, obj)
end
obj
end
def self.load_by_slug(slug, shouldBeType, ctx, permission, must_exist = true)
self.load_by(:slug, slug, shouldBeType, ctx, permission, must_exist)
end
def self.load_by(key, key_value, shouldBeType, ctx, permission, must_exist = true)
user = ctx[:current_user] || User.new
obj = shouldBeType.find_by(key => key_value)
if must_exist && obj.nil?
raise Exceptions::GraphExecutionError.new('Object does not exist')
end
if obj.present?
raise Exceptions::GraphExecutionError.new('Key did not return the proper type') unless obj.class == shouldBeType
raise Exceptions::GraphExecutionError.new('User does not have permission to perform operation') unless user.can?(permission, obj)
end
obj
end
end
I'm using CanCan::Ability where there's delegates on the User model.
delegate :can?, :cannot?, to: :ability
Then my usage looks like this:
field :experience do
type ExperienceGraph
description 'Find an experience'
argument :slug, !types.String
resolve -> (obj, args, ctx) {
GraphLoader.load_by_slug(args[:slug], Experience, ctx, :read)
}
end
For my needs, I never needed to check an incoming Node ID type against anything other than just one. But this code could be adapted to accept an array as the shouldBeType.
You should definitely do type checking of some sort to see if the returned object support what you want to do or whether the user has permission to do such a thing.
You can simply use the
obj = GraphSchema.object_from_id(objectId, ctx)
to fetch the object, but you need to absolutely check that someone isn't hacking your system by loading another type of object. But the above call will return an object with the GraphQL Object ID, then you just need some instrumentation around making sure the user making that request should be able to do whatever is about to happen.
Your code would look something like this:
Upvote = GraphQL::Relay::Mutation.define do
name 'Upvote'
input_field :ID, !types.ID
return_field :success, !types.Boolean
resolve -> (obj, args, ctx) {
to_up = GraphSchema.object_from_id(args[:ID], ctx)
if to_up.present? && (to_up.is_a?(Post) || to_up.is_a?(Comment))
to_up.perform_upvote
else
raise Exceptions::GraphExecutionError.new('Cannot upvote this class') # only happens if being hacked or if a new class is able to be upvoted.
end
{success: true}
}
end
If you implement the upvoting as a module or similar, you could just test the returned object that it can respond to the appropriate call, if it can, then send it through rather than explicitly naming every type.
if to_up.present? && (to_up.is_a?(Post) || to_up.is_a?(Comment))
becomes
if to_up.present? && to_up.respond_to?(:perform_upvote)
:+1: to @nuclearspike 's solution. If you're using class-based mutations, you get basically the same behavior with loads:
class Mutations::Upvote < Mutations::Base
argument :id, ID, required: true, loads: Types::Upvotable, as: :upvotable
def resolve(upvotable:)
# `upvotable` will be loaded with `Schema.object_from_id` and checked using `Upvotable.resolve_type`.
# If the load fails, an error is returned to the client
end
end
Thanks for including this info! I have been using this gem long enough and haven't been following the best practices for the better ways to do stuff that I didn't even know mutation classes were a thing. Is there any integration with cancan/cancancan to let you define the action on the argument to have it auto-check if you have permission to do what's about to happen? The idea is that it takes current user and checks if it has the permission defined by "can:" With cancan/cancancan, old-fashioned controller updates happen via
load_and_authorize_resource
or breaking it up if using a different id, like
load_resource find_by: :slug
authorize_resource
But rounding out this gem's offering, it would be great to have ability-integration. Does that already exist? Or is it up to the dev to check before performing an operation?
Something like:
class Mutations::Upvote < Mutations::Base
argument :id, ID, required: true, loads: Types::Upvotable, as: :upvotable, can: :upvote
def resolve(upvotable:)
# `upvotable` will be loaded with `Schema.object_from_id` and checked using `Upvotable.resolve_type`.
# If the load fails, an error is returned to the client
end
end
Where the ability.rb file would have similar to (prevents user from upvoting their own post/comment):
if @user.has_account?
can :upvote, [Post, Comment] do |upvotable|
upvotable.user != @user
end
...
end
A cancan integration is offered as part of GraphQL-Pro (https://graphql-ruby.org/authorization/can_can_integration), but it's built on top of the core authorization system (http://graphql-ruby.org/authorization/authorization.html), so you could just as well build one yourself, too!
I've also seen similar third-party libraries:
Most helpful comment
@rmosolgo, could you possibly give an example on how polymorphism in mutations would be implemented on the server? For example an
upvoteMutationthat can be used for both aPostandComment?