dchuk/rails_ai_agents · Archived

rails-query-object

Creates query objects for complex database queries following TDD. Use when encapsulating complex queries, aggregating statistics, building reports, or when user mentions queries, stats, dashboards, or data aggregation.

First seen Feb 13, 2026

Installation

$ npx skills add dchuk/rails_ai_agents --skill rails-query-object

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from dchuk/rails_ai_agents · top by installs.

npx skills add dchuk/rails_ai_agents

Browse all from dchuk/rails_ai_agents

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsRead, Write, Edit, Bash, Glob, Grep

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 6,852 B
  • docs SUMMARY.md 244 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

Rails Query Object Generator (TDD)

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

Quick Start

  1. Write failing test in test/queries/
  2. Run test to confirm RED
  3. Implement query object in app/queries/
  4. Run test to confirm GREEN

When to Use Query Objects vs Scopes

Scenario Use
Simple WHERE clause Scope on the model
Single-condition filter Scope on the model
Multi-table joins with conditions Query object
Dashboard aggregations Query object
Report generation Query object
Queries needing constructor params Query object
Reusable across controllers Query object

Rule of thumb: If the query fits in one line and needs no context, use a scope. If it needs parameters, joins multiple tables, or returns computed data, use a query object.

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 Test (RED)

# test/queries/stale_leads_query_test.rb
require "test_helper"

class StaleLeadsQueryTest < ActiveSupport::TestCase
  setup do
    @account = accounts(:one)
    @other_account = accounts(:two)
  end

  test "requires an account parameter" do
    assert_raises(ArgumentError) { StaleLeadsQuery.new }
  end

  test "#call returns ActiveRecord::Relation" do
    query = StaleLeadsQuery.new(account: @account)
    assert_kind_of ActiveRecord::Relation, query.call
  end

  test "#call returns only leads for the account (multi-tenant)" do
    own_lead = leads(:stale_one)
    other_lead = leads(:other_account_stale)

    results = StaleLeadsQuery.new(account: @account).call

    assert_includes results, own_lead
    assert_not_includes results, other_lead
  end

  test "#call returns only stale leads" do
    stale = leads(:stale_one)
    fresh = leads(:fresh_one)

    results = StaleLeadsQuery.new(account: @account).call

    assert_includes results, stale
    assert_not_includes results, fresh
  end

  test "multi-tenant isolation" do
    other_query = StaleLeadsQuery.new(account: @other_account)
    own_query = StaleLeadsQuery.new(account: @account)

    assert_empty(other_query.call.where(id: leads(:stale_one).id))
    assert_not_empty(own_query.call.where(id: leads(:stale_one).id))
  end
end

Step 2: Run Test (Confirm RED)

bin/rails test test/queries/stale_leads_query_test.rb

Step 3: Implement Query Object (GREEN)

# app/queries/stale_leads_query.rb
class StaleLeadsQuery
  attr_reader :account

  def initialize(account:)
    @account = account
  end

  def call
    account.leads.stale
  end
end

Step 4: Run Test (Confirm GREEN)

bin/rails test test/queries/stale_leads_query_test.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(&:status).each do |status, status_leads|
      result[status.to_sym] = status_leads
    end

    result
  end
end

Testing Aggregation Queries

# test/queries/dashboard_stats_query_test.rb
require "test_helper"

class DashboardStatsQueryTest < ActiveSupport::TestCase
  setup do
    @user = users(:one)
    @query = DashboardStatsQuery.new(user: @user)
  end

  test "#upcoming_events returns future events only" do
    results = @query.upcoming_events
    results.each do |event|
      assert event.event_date >= Date.today
    end
  end

  test "#upcoming_events respects limit" do
    results = @query.upcoming_events(limit: 2)
    assert results.size <= 2
  end

  test "#leads_by_status returns hash of status to count" do
    result = @query.leads_by_status
    assert_kind_of Hash, result
  end

  test "scoped to user account only" do
    other_user = users(:other_account)
    other_query = DashboardStatsQuery.new(user: other_user)

    own_events = @query.upcoming_events
    other_events = other_query.upcoming_events

    own_events.each do |event|
      assert_equal @user.account_id, event.account_id
    end
  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

Directory Structure

app/queries/
  stale_leads_query.rb
  leads_by_status_query.rb
  dashboard_stats_query.rb
  events/
    upcoming_query.rb
    by_vendor_query.rb
test/queries/
  stale_leads_query_test.rb
  dashboard_stats_query_test.rb

Checklist

  • Test written first (RED)
  • Constructor accepts context (user: or account:)
  • Multi-tenant isolation tested
  • Return type documented
  • Methods have clear, descriptive names
  • Complex queries use .includes() to prevent N+1
  • Database-agnostic (no PostgreSQL-specific SQL)
  • All tests GREEN