rails-query-object

Rails Query Object Generator (TDD)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rails-query-object" with this command: npx skills add thibautbaissac/rails_ai_agents/thibautbaissac-rails-ai-agents-rails-query-object

Rails Query Object Generator (TDD)

Creates query objects that encapsulate complex database queries with specs first.

Quick Start

  • Write failing spec in spec/queries/

  • Run spec to confirm RED

  • Implement query object in app/queries/

  • Run spec to confirm GREEN

Project Conventions

Query objects in this project:

  • Accept context via constructor (user: or account: )

  • Return ActiveRecord::Relation for chainability OR Hash for aggregations

  • Have a call method for primary operation

  • Support multi-tenancy (scoped to account)

TDD Workflow

Step 1: Create Query Spec (RED)

spec/queries/[name]_query_spec.rb

RSpec.describe [Name]Query do subject(:query) { described_class.new(account: account) }

let(:user) { create(:user) } let(:account) { user.account } let(:other_account) { create(:user).account }

Test data for current account

let!(:resource1) { create(:resource, account: account) } let!(:resource2) { create(:resource, account: account) }

Test data for other account (should not appear)

let!(:other_resource) { create(:resource, account: other_account) }

describe "#initialize" do it "requires an account parameter" do expect { described_class.new }.to raise_error(ArgumentError) end

it "stores the account" do
  expect(query.account).to eq(account)
end

end

describe "#call" do it "returns expected result type" do expect(query.call).to be_a(ActiveRecord::Relation) # OR for hash results: # expect(query.call).to be_a(Hash) end

it "only returns resources for the account (multi-tenant)" do
  result = query.call
  expect(result).to include(resource1, resource2)
  expect(result).not_to include(other_resource)
end

end

describe "multi-tenant isolation" do it "ensures account A cannot see account B data" do other_query = described_class.new(account: other_account)

  expect(query.call).not_to include(other_resource)
  expect(other_query.call).not_to include(resource1)
end

end end

Step 2: Run Spec (Confirm RED)

bundle exec rspec spec/queries/[name]_query_spec.rb

Step 3: Implement Query Object (GREEN)

app/queries/[name]_query.rb

class [Name]Query attr_reader :account

def initialize(account:) @account = account end

Returns [description of result]

@return [ActiveRecord::Relation<Resource>] OR [Hash]

def call account.resources .where(condition: value) .order(created_at: :desc) end end

Step 4: Run Spec (Confirm GREEN)

bundle exec rspec spec/queries/[name]_query_spec.rb

Query Object Patterns

Pattern 1: Simple Filtered Query

app/queries/stale_leads_query.rb

class StaleLeadsQuery attr_reader :account

def initialize(account:) @account = account end

def call account.leads.stale end end

Pattern 2: Aggregation Query (Multiple Methods)

app/queries/dashboard_stats_query.rb

class DashboardStatsQuery attr_reader :user, :account

def initialize(user:) @user = user @account = user.account end

def upcoming_events(limit: 3) account.events .where("event_date >= ?", Date.today) .order(event_date: :asc) .limit(limit) end

def pending_commissions_total EventVendor .joins(:event) .where(events: { account_id: account.id }) .where(commission_status: :to_invoice) .sum(:commission_value) end

def top_vendors(limit: 5) account.vendors .left_joins(:event_vendors) .select("vendors.*, COUNT(event_vendors.id) as events_count") .group("vendors.id") .order("events_count DESC") .limit(limit) end

def leads_by_status account.leads.group(:status).count end end

Pattern 3: Grouping Query

app/queries/leads_by_status_query.rb

class LeadsByStatusQuery attr_reader :account

def initialize(account:) @account = account end

def call leads = account.leads.order(created_at: :desc) result = Lead.statuses.keys.map(&:to_sym).index_with { [] }

leads.group_by(&#x26;:status).each do |status, status_leads|
  result[status.to_sym] = status_leads
end

result

end end

Usage in Controllers

Simple query

def index @leads_by_status = LeadsByStatusQuery.new(account: current_account).call end

Aggregation query with presenter

def index stats_query = DashboardStatsQuery.new(user: current_user) @stats = DashboardStatsPresenter.new(stats_query) end

Checklist

  • Spec written first (RED)

  • Constructor accepts context (user: or account: )

  • Multi-tenant isolation tested

  • Return type documented (@return )

  • Methods have clear, descriptive names

  • Complex queries use .includes() to prevent N+1

  • All specs GREEN

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

hotwire-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

i18n-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

rails-controller

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

rails-architecture

No summary provided by upstream source.

Repository SourceNeeds Review