Step 9: Polymorphic Resources

View the Diff

In the last step, we covered polymorphic relationships: a single relationship can point to many different Resources. Polymorphic Resources are the same concept, without an association: a single Resource can resolve to many different sub-Resources. It’s a very similar to Single-Table Inheritance in ActiveRecord.

To illustrate this, we’ll add a tasks table and corresponding Task superclass. Each record in this table will resolve to one of Bug, Epic, or Feature.

id milestone_id type title
1 null Bug Incorrect Value!
2 null Feature Build great stuff!
3 1 Epic Build TONS of great stuff!

Why not just stick with a single Task model? Because each of these types has specific behavior: only Features have a points attribute, and only Epics have a milestones relationship.

The Rails Stuff 🚂

Let’s create our Task model:

$ bin/rails g model Task employee:belongs_to team:belongs_to type:string
title:string
$ bin/rails db:migrate

And create models to reflect our STI logic:

# app/models/task.rb
class Task < ApplicationRecord
  TYPES = %w(Bug Feature Epic)

  belongs_to :team, optional: true
  belongs_to :employee, optional: true
end

# app/models/bug.rb
class Bug < Task
end

# app/models/feature.rb
class Feature < Task
end

# Only Epics have Milestones
# app/models/epic.rb
class Epic < Task
  has_many :milestones
end

# app/models/milestone.rb
class Milestone < ApplicationRecord
  belongs_to :epic
end

Add the association:

# app/models/team.rb
has_many :tasks
has_many :bugs
has_many :features
has_many :epics

# app/models/employee.rb
has_many :tasks
has_many :bugs
has_many :features
has_many :epics

Finally view the diff to edit your seeds.rb file.

The Graphiti Stuff 🎨

Start by creating our Resource as normal:

$ bin/rails g graphiti:resource Task title:string

Now edit to support polymorphism and associations:

class TaskResource < ApplicationResource
  self.polymorphic = %w(FeatureResource BugResource EpicResource)

  attribute :employee_id, :integer, only: [:filterable]
  attribute :team_id, :integer, only: [:filterable]
  attribute :title, :string

  belongs_to :employee
  belongs_to :team
end

The point of this was to show how responses could be specific to type, so let’s customize Features:

class FeatureResource < TaskResource
  attribute :points, :integer do
    rand(20)
  end
end

Only Epics have milestones, but let’s support those as well:

$ bin/rails g graphiti:resource Milestone name:string
class MilestoneResource < ApplicationResource
  attribute :epic_id, :integer, only: [:filterable]
  attribute :name, :string

  # Customize the link to the Tasks endpoint, as we
  # didn't create an Epics endpoint
  belongs_to :epic do
    link do |milestone|
      helpers = Rails.application.routes.url_helpers
      helpers.task_url(milestone.epic_id)
    end
  end
end

Digging Deeper 🧐

We can now resolve Tasks, either as a relationship or through the /tasks endpoint directly. When Task is type 'Feature' it will have an extra attribute of points. When it’s an Epic, it will have an additional relationship Milestone.

Graphiti is smart enough to fetch the appropriate relationships. A hit to /tasks?include=milestones will only query for milestones when the resulting Task records are Epics.