Is there a good (read: D.R.Y.) way to Decorate objects?
Right now, we have some (ugly) code looking like this:
UserType = ::GraphQL::ObjectType.define do
name "User"
description "I am user"
...
field :profile_picture_url do
type !types.String
argument :size, !types.Int, default_value: 50
resolve -> (obj, args, ctx) {
UserDecorator.new(obj).profile_picture_url(size: args[:size])
}
end
...
end
So I'm thinking something like coerce, but for object types:
UserType = ::GraphQL::ObjectType.define do
name "User"
description "I am user"
decorate -> (obj) { UserDecorator.new(obj) }
...
field :profile_picture_url do
type !types.String
argument :size, !types.Int, default_value: 50
resolve -> (obj, args, ctx) {
obj.profile_picture_url(size: args[:size])
}
end
...
end
I've naively tried to do this using coerce, but it doesn't seem to work for object types.
I though of a few options, do any of them strike your fancy?
ruby
prof_pic_url = ProfilePictureUrl.new(user, args[:size)
prof_pic_url.to_s # returns the URL
I like this pattern because the objects have small, testable interfaces (unlike decorators, which often take an already-large public API and make it larger). But of course, if you already have a bunch of decorators, this doesn't do ya much good
resolve procs. This is how my connection helper works in my Relay library. ``ruby
# Wrap each oftype`'s fields to inject a decorated object
def decorate_type(decorator_class, type)
type.fields.each do |name, field_defn|
inner_resolve = field_defn.resolve_proc
outer_resolve = wrap_resolve_with_decorator(decorator_class, inner_resolve)
field_defn.resolve_proc = outer_resolve
end
type
end
# This new proc makes a decorated object and passes it to the inner proc
def wrap_resolve_with_decorator(decorator_class, inner_resolve)
-> (obj, args, ctx) {
decorated_obj = decorator_class.new(obj)
inner_resolve.call(decorated_obj, args, ctx)
}
end
# After defining a type, pass it to decorate_type with a decorator
UserType = decorate_type UserDecorator, GraphQL::ObjectType.define { ... }
```
This works with minimal boilerplate, but the implementation is so wasteful! It makes a new instance of the decorator for each resolved field.
UserType, decorate in any field which returns a UserType. ``` ruby
TeamType = GraphQL::ObjectType.define do
field :leader, UserType do
resolve -> (obj, args, ctx) { UserDecorator.new(obj) }
end
field :members, types[UserType] do
resolve -> (obj, args, ctx) { obj.map { |user| UserDecorator.new(obj) }
end
end
```
Again, not too much code. But you might lose some power in ListType fields. Now, it returns an Array instead of an ActiveRecord::Relation, so pagination & filtering is much less efficient.
``ruby
class DecorationMiddleware
# decorator_map matches object classes to their decorators.
# not sure if keys should be classes or strings, depends on Rails reloading stuffs
#
# You might not need this if you can fetch decorators by name, eg"#{record.class.name}Decorator".constantize`
def initialize(decorator_map)
@decorator_map = decorator_map
end
def call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
# let the field resolve:
result = next_middleware.call
# fetch & apply decorator
decorator_class = @decorator_map[result.class]
if decorator_class
result = decorator_class.new(result)
end
# return the maybe-decorated result
result
end
end
```
Add the middleware to your schema:
ruby
MySchema = GraphQL::Schema.new # ...
MySchema.middleware << DecorationMiddleware.new({
User => UserDecorator,
Team => TeamDecorator
})
That's not too much code and stays out of the way. I've never tried a middleware like that but it seems like it would work!
Thanks for the thorough and insightful response :) I was hoping I could use the middleware approach, just wasn't sure how.
I'll try and apply these to my project and see which approach fits me best.
I tried the middleware approach but couldn't make it work. From https://github.com/rmosolgo/graphql-ruby/issues/2479#issuecomment-531576253 I found Field Extension is an alternative.
The Field Extension approach would be something like:
```ruby,diff
class DecorationExtension < GraphQL::Schema::FieldExtension
def after_resolve(value:, **options)
decorator_class = options[:decorator_class] || value.decorator_class
# If you're using Draper. Use `comments.map(&:decorate)` instead of the `comments.decorate` shorthand otherwise AssociationLoader won't work properly.
if value.respond_to? :map
value.map{|v| decorator_class.new(v) }
else
decorator_class.new(value)
end
end
end
module Types
class ArticleType < Types::BaseObject
field :comments, [Types::CommentType], 'All comments of this article', extensions: [DecorationExtension]
field :likes, [Types::LikeType], 'All comments of this article' do
extension(DecorationExtension, decorator_class: LikeDecorator)
end
# if you are using graphql-batch
def comments
Dataloader::AssociationLoader.for(Article, :comments).load(object)
end
end
end
module Types
class ArticleType < Types::BaseObject
field :a_method_of_comment, ...
field :a_medhot_of_comment_decorator, ...
end
end
```
Most helpful comment
I though of a few options, do any of them strike your fancy?
ruby prof_pic_url = ProfilePictureUrl.new(user, args[:size) prof_pic_url.to_s # returns the URLI like this pattern because the objects have small, testable interfaces (unlike decorators, which often take an already-large public API and make it larger). But of course, if you already have a bunch of decorators, this doesn't do ya much good
resolveprocs. This is how myconnectionhelper works in my Relay library.``
ruby # Wrap each oftype`'s fields to inject a decorated objectdef decorate_type(decorator_class, type)
type.fields.each do |name, field_defn|
inner_resolve = field_defn.resolve_proc
outer_resolve = wrap_resolve_with_decorator(decorator_class, inner_resolve)
field_defn.resolve_proc = outer_resolve
end
type
end
# This new proc makes a decorated object and passes it to the inner proc
def wrap_resolve_with_decorator(decorator_class, inner_resolve)
-> (obj, args, ctx) {
decorated_obj = decorator_class.new(obj)
inner_resolve.call(decorated_obj, args, ctx)
}
end
# After defining a type, pass it to
decorate_typewith a decoratorUserType = decorate_type UserDecorator, GraphQL::ObjectType.define { ... }
```
This works with minimal boilerplate, but the implementation is so wasteful! It makes a new instance of the decorator for each resolved field.
UserType, decorate in any field which returns aUserType.``` ruby
TeamType = GraphQL::ObjectType.define do
field :leader, UserType do
resolve -> (obj, args, ctx) { UserDecorator.new(obj) }
end
end
```
Again, not too much code. But you might lose some power in
ListTypefields. Now, it returns an Array instead of an ActiveRecord::Relation, so pagination & filtering is much less efficient.``
ruby class DecorationMiddleware # decorator_map matches object classes to their decorators. # not sure if keys should be classes or strings, depends on Rails reloading stuffs # # You might not need this if you can fetch decorators by name, eg"#{record.class.name}Decorator".constantize`def initialize(decorator_map)
@decorator_map = decorator_map
end
end
```
Add the middleware to your schema:
ruby MySchema = GraphQL::Schema.new # ... MySchema.middleware << DecorationMiddleware.new({ User => UserDecorator, Team => TeamDecorator })That's not too much code and stays out of the way. I've never tried a middleware like that but it seems like it would work!