Overview

A Resource queries and persists to a Backend. It returns Models from the Backend response, which get serialized. In this way, it is an implementation of the Repository Pattern.

This is best illustrated in code. Let’s say we have a Backend class that accepts a hash of options to perform a query:

results = Backend.query \
  conditions: { name: 'Jane' },
  sort: { created_at: :asc }

results # [{ id: 1, name: 'Jane', rank: 83 }, ...]

And a PORO Model that encapsulates those results, holding business logic:

class Employee
  attr_accessor :id, :name, :rank

  def initialize(attrs = {})
    attrs.each_pair { |k, v| send(:"#{k}=", v) }
  end

  def exemplary?
    rank > 80
  end
end

Meaning that normally, our code would look something like:

results = Backend.query(params)
employees = results.map { |r| Employee.new(r) }
employees.map(&:exemplary?) # => [true, false, ...]

Let’s wire-up that same code to a Resource:

class EmployeeResource < ApplicationResource
  # We'll be coding the logic manually
  self.adapter = Graphiti::Adapters::Null

  attribute :name, :string

  # The blank scope we start with
  def base_scope
    { conditions: {}, sort: {}  }
  end

  # Merge filters into the hash based on request params
  filter :name do
    eq do |scope, value|
      scope[:conditions].merge!(value)
      scope
    end
  end

  # Set sort based on request params
  sort :name do |scope, direction|
    scope[:sort] = { name: direction }
    scope
  end

  # 'scope' here is our hash
  # We pass it to Backend.query, and return Models
  def resolve(scope)
    results = Backend.query(scope)
    results.map { |r| Employee.new(r) }
  end
end

As you see above, a scope can be anything from an ActiveRecord::Relation to a plain Ruby Hash. We want to adjust something based on the request parameters and pass it to our backend. From the raw backend results, we can instantiate Models. Note that we always return the full scope at the end of each block.

Of course, most Backends have predictable and consistent interfaces. It would be a pain to manually write this code for every Resource. So instead we could build an Adapter to DRY this logic:

class EmployeeResource < ApplicationResource
  self.adapter = BackendAdapter
  attribute :name, :string
end

In summary: a Resource builds a query that is sent to a Backend. The backend executes the query, and we instantiate Models from the raw results.

ActiveRecord

From the ActiveRecord Guides:

Active Record was described by Martin Fowler in his book Patterns of Enterprise Application Architecture. In Active Record, objects carry both persistent data and behavior which operates on that data. Active Record takes the opinion that ensuring data access logic as part of the object will educate users of that object on how to write to and read from the database.

In other words, ActiveRecord combines a Backend and Model. Opinions on this vary, but Graphiti supports either approach: we can separate data and business layers, or combine them. See the ActiveRecord doppelgänger of the above at our Resource cheatsheet.

Model Requirements

The only hard requirement of a Model is that it responds to id. We use model.id to determine uniqueness when rendering a JSONAPI response. You will get incorrect results if model.id is not unique.

Models should also respond to any readable attributes. Remember that:

attribute :name, :string

Is the same as

# @object is your Model instance
attribute :name, :string do
  @object.name
end

If your Model does not respond to #name, either pass a block to attribute or look into aliasing.

Validations

Graphiti will perform validations on your models during write requests, returning a JSONAPI-compliant errors payload. To get this functionality, your model must adhere to the ActiveModel::Validations API:

model.valid?
object.errors.messages.each_pair { ... }

It is highly recommended to mix in:

class Employee
  include ActiveModel::Validations
end

Model Implementations

Because our default is ActiveRecord, it may be unclear what other Models look like. Graphiti itself has no opinion about your Model layer, but below are a few examples.

2.1 PORO

class Employee
  attr_accessor :id,
    :first_name,
    :last_name,
    :age

  def initialize(attrs = {})
    attrs.each_pair { |k,v| send(:"#{k}=", v) }
  end
end

This is a common Ruby example. attr_accessor defines getters and setters for our properties, and we assign those properties in the constructor:

e = Employee.new(id: 1, first_name: 'Jane')
e.first_name # => 'Jane'

2.2 ActiveModel::Model

A simple abstraction of the above is ActiveModel::Model:

class Employee
  include ActiveModel::Model

  attr_accessor :id,
    :first_name,
    :last_name,
    :age
end
e = Employee.new(id: 1, first_name: 'Jane')
e.first_name # => 'Jane'

2.3 Dry::Struct

dry-types is a dependency of Graphiti and successor to the popular Virtus.

module Types
  include Dry::Types.module
end

class Employee < Dry::Struct
  attribute :id, Types::Integer
  attribute :first_name, Types::String
  attribute :last_name, Types::Integer
  attribute :age, Types::Integer
end
e = Employee.new(id: 1, first_name: 'Jane')
e.first_name # => 'Jane'

3 Model Tips

ID-less Models

If your Model does not have an id property, using a random UUID is perfectly acceptable:

def id
  @id ||= SecureRandom.uuid
end