Customizing Sideloads

See the code in our sample app

This cookbook will help you understand sideloading. It would be great to live in a world where everything follows default ActiveRecord table conventions, but in my experience this is rarely the case. From legacy code to alternate datastores, we need to think in Real World terms.

Our Employee Directory sample application has a clean schema - let’s screw with it. Let’s say Department has a column called watcher_emails, which is an array of strings. We want to sideload Department > Watchers. Though the relationship is called watchers, these will be Employee records.

Let’s start by adding a spec:

# spec/resources/department/reads_spec.rb

describe 'sideloading' do
  describe 'watchers' do
    let!(:employee1) { create(:employee) }
    let!(:employee2) { create(:employee) }
    let!(:employee3) { create(:employee) }
    let!(:department) do
      create :department,
        watcher_emails: [,]

    before do
      params[:include] = 'watchers'

    it 'sideloads employees via watcher_emails' do
      sl = d[0].sideload(:watchers)
      expect( eq([,])
      expect( eq(['employees'])

Add the relationship:

# app/resources/department_resource.rb
has_many :watchers, resource: EmployeeResource

Run the test and you’ll get this error:

  EmployeeResource: Tried to filter on attribute :department_id, but could not find an attribute with that name.

How would we track down this error? Well, we know Resources connect together with Links. Let’s take a look at the query parameters that would be used to connect these two Resources:

has_many :watchers, resource: EmployeeResource do
  params do |hash, departments|

Note - we’re using pry to debug here.

The value of hash here is:

{ filter: { department_id: "1" } }

Which makes sense. If we say has_many :things, by default we expect Thing to have a department_id we can query.

That’s not our case, though. Instead, let’s customize those parameters to fit our use case:

params do |hash, departments|
  emails =
  hash[:filter] = { email: emails }

Instead of querying by department_id, we need to query by email. And the value we pass in will be an array of email addresses

We’d need to add an email filter to EmployeeResource to make this work. This gets us querying correctly, but there’s another error:

undefined method `department_id' for #<Employee:0x00007f9652ae6d80>

Here’s the thing to keep in mind: let’s say our request was /departments?include=watchers. We queried all the data, and we now have an array of Departments and an array of Employees. Now we need to specify which employees should be assigned as watchers of which department.

Let’s write that code manually:

has_many :watchers, resource: EmployeeResource do
  # ... code ...
  assign do |departments, employees|
    departments.each do |d|
      d.watchers = do |e|

We’re selecting all relevant Employees for a given Department by checking the array of watcher_emails.

This code can be tightened up a little with assign_each (recommended). This way we don’t have to iterate departments or worry about the assignment ourselves:

has_many :watchers, resource: EmployeeResource do
  # ... code ...

  assign_each do |department, employees| { |e| }

Note that we’re using #select to return an array of relevant Employees. If this was a belongs_to or has_one relationship, we’d probably want to use #find to return a single Employee.

OK there’s one last error:

undefined method `watchers=' for #<Department:0x00007feb625a7468>

This one is simple - the assign function will call your Adapter’s assignment logic, which by default will be a simple department.watchers = relevant_employees. That means we need to add a getter/setter for this property:

# app/models/department.rb
attr_accessor :watchers

And we’re done! The test should now pass. Check out the working code here.