Rails scopes are a useful feature. We can define biz logic in the scopes and use them from controller actions or other model methods. We can also pass parameters into scopes and chain scopes together. I am not going to go into all the options but instead share how I recently started using scopes inside other scopes in the models.

Let’s imagine a system where we have Accounts that can be active or pending. We also created several special demo accounts in our production DB and listed their IDs in a config file (applying special biz logic to them). We want to automaticaly exclude these demo accounts from our active and pending business workflows.

# config/application.rb
config.demo_accounts = [id1, id2]
# app/models/account.rb
field :status,          type: String
extend Enumerize
enumerize :status, in: [:active, :pending]
scope :demo,       ->{ where(:_id.in  => Rails.application.config.demo_accounts) }
scope :not_demo,   ->{ where(:_id.nin => Rails.application.config.demo_accounts) }
scope :active,     ->{ where(status: :active)  }
scope :pending,    ->{ where(status: :inactive) }

One option is to chain scopes active.not_demo but we might forget in our application code. Or we can modify default_scope to exclude demo but that is usually not recommended. Instead we can simply chain scopes inside the scope. Now whenever we call .active or .pending in our code the demo accounts will be excluded.

# app/models/account.rb
scope :active,     ->{ where(status: :active).not_demo  }
scope :pending,    ->{ where(status: :inactive).not_demo }

Or we can chain multiple scopes inside ->{ ... } to create new scopes.

# app/models/account.rb
field :important,        type: Boolean
scope :important,        ->{ where(important: :true) }
scope :active,           ->{ where(status: :active)  }
scope :important_active, ->{ important.active }

We can test scopes like this:

# spec/models/account_spec.rb
it 'scopes' do
  account = create(:account)
  expect(Account.active.count).to eq 0
  account.update(status: :active)
  expect(Account.active.count).to eq 1
end

Here is a link to a post that inspired me to write this. So that’s it. Short post but I thought it was interesting.