GraphQL Decorate

graphql-decorate adds an easy-to-use interface for decorating types in graphql-ruby. It lets you move logic out of your type files and keep them declarative.

Installation

Add this line to your application’s Gemfile:

gem 'graphql-decorate'

And then execute:

$ bundle

Or install it yourself as:

$ gem install graphql-decorate

Once the gem is installed, you need to add the plugin to your schema and the integration into your base object class.

class Schema < GraphQL::Schema
  use GraphQL::Decorate
end

class BaseObject < GraphQL::Schema::Object
  include GraphQL::Decorate::ObjectIntegration
end

Note that use GraphQL::Decorate must be included in the schema after query and mutation so that the fields to be extended are initialized first.

Usage

Basic use case

class Rectangle
  attr_reader :length

  def initialize(length)
    @length = length
  end
end

class RectangleDecorator < BaseDecorator
  def area
    length * 2
  end
end

class RectangleType < BaseObject
  decorate_with RectangleDecorator
  
  field :area, Int, null: false
end

In this example, the Rectangle type is being decorated with a RectangleDecorator. Whenever a Rectangle gets resolved in the graph, the underlying object will be wrapped with a RectangleDecorator. All of the methods on the decorator are accessible on the type.

Decorators

By default, graphql-decorate is set up to work with draper style decorators. These decorators provide a decorate method that wraps the original object and returns an instance of the decorator. They can also take in additional metadata.

RectangleDecorator.decorate(rectangle, context: metadata)

If you are using a different decorator pattern then you can override this default behavior in the configuration.

GraphQL::Decorate.configure do |config|
  config.decorate do |decorator_class, object, _metadata|
    decorator_class.decorate_differently(object)
  end
end

Types

Two methods are made available on your type classes: decorate_with and decorate_metadata. Every method that yields the underlying object will also yield the current GraphQL context. If decoration depends on some context in the current query then you can access it when the field is resolved.

decorate_with

decorate_with accepts a decorator class that will decorate every instance of your type.

class Rectangle < GraphQL::Schema::Object
  decorate_with RectangleDecorator
end

decorate_with optionally accepts a block which yields the underlying object. If you have multiple possible decorator classes you can return the one intended for the underling object.

class Rectangle < GraphQL::Schema::Object
  decorate_with do |object, _graphql_context|
    if object.length == object.width
      SquareDecorator
    else
      RectangleDecorator
    end
  end
end

decorate_metadata

If your decorator pattern allows additional metadata to be passed into the decorators, you can define it here. By default every metadata hash will contain { graphql: true }. This is useful if your decorator logic needs to diverge when used in a GraphQL context. Ideally your decorators are agnostic to where they are being used, but it is available if needed.

decorate_metadata yields a GraphQL::Decorate::Metadata metadata instance. It responds to two methods: unscoped and scoped. unscoped sets metadata for a resolved field. scoped sets metadata for a resolved field and all of its child fields. unscoped and scoped are expected to return Hashs.

class Rectangle < GraphQL::Schema::Object
  decorate_metadata do |metadata| 
    metadata.unscoped do |object, _graphql_context| 
      { 
        name: object.name
      }
    end
   
    metadata.scoped do |object, _graphql_context|
      {
        inside_rectangle: true
      }
    end
  end
end

RectangleDecorator will be initialized with metadata { name: <object_name>, inside_rectangle: true, graphql: true }. All child fields of Rectangle will be initialized with metadata { inside_rectangle: true, graphql: true }.

Combinations

You can mix and match these methods to suit your needs. Note that if unscoped and scoped are both provided for metadata that scoped will override any shared keys.

class Rectangle < GraphQL::Schema::Object
  decorate_with RectangleDecorator
  decorate_metadata do |metadata|
    metadata.scoped do |object, _graphql_context|
      {
        name: object.name
      } 
    end
  end
end

Collections

By default graphql-decorate recognizes Array and ActiveRecord::Relation object types and decorates every element in the collection. If you have other collection types that should have their elements decorated, you can add them in the configuration. Custom collection classes must respond to #map.

GraphQL::Decorate.configure do |config|
  config.custom_collection_classes = [Mongoid::Relations::Targets::Enumerable]
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

License

The gem is available as open source under the terms of the MIT License.