Graphql-ruby: Decorating types

Created on 15 Dec 2015  路  3Comments  路  Source: rmosolgo/graphql-ruby

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.

Most helpful comment

I though of a few options, do any of them strike your fancy?

  • Don't decorate, use function objects instead. If you need a value which is a function of a user and some other data, create an object which takes those inputs and returns the value. Eg,

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

  • Wrap the field's 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.

  • Decorate in the parent type. Instead of decorating in 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.

  • Decorate with middleware. After resolving a a field, check if the value should be decorated.

``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!

All 3 comments

I though of a few options, do any of them strike your fancy?

  • Don't decorate, use function objects instead. If you need a value which is a function of a user and some other data, create an object which takes those inputs and returns the value. Eg,

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

  • Wrap the field's 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.

  • Decorate in the parent type. Instead of decorating in 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.

  • Decorate with middleware. After resolving a a field, check if the value should be decorated.

``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

decoration_extension.rb

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

types/article_type.rb

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

types/comment_type.rb

module Types
class ArticleType < Types::BaseObject
field :a_method_of_comment, ...
field :a_medhot_of_comment_decorator, ...
end
end
```

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rylanc picture rylanc  路  3Comments

KevinColemanInc picture KevinColemanInc  路  3Comments

jtippett picture jtippett  路  3Comments

sayduck-daniel picture sayduck-daniel  路  3Comments

ecomuere picture ecomuere  路  3Comments