0.0
The project is in a healthy, maintained state
ActiveQuery is a gem that helps you to create query objects in a simple way. It provides a DSL to define queries and scopes for your query object.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

~> 11.0
~> 13.0
~> 3.9.0
~> 0.17.1
~> 1.5.1

Runtime

>= 6.1, < 8.0
>= 6.1, < 8.0
 Project Readme

ActiveQuery

ActiveQuery is a Ruby gem that helps you create clean, reusable query objects with a simple DSL. It provides type validation, conditional logic, and seamless ActiveRecord integration.

Features

  • Clean Query DSL: Define queries with clear syntax and descriptions
  • Type Safety: Built-in argument type validation (String, Integer, Float, Boolean)
  • Optional & Default Arguments: Flexible argument handling
  • Custom Operations: Extended query operations like gt, lt, like, etc.
  • Conditional Logic: Apply scopes conditionally with if and unless
  • Resolver Pattern: Support for complex query logic in separate classes
  • Custom Scopes: Define reusable scopes within query objects
  • ActiveRecord Integration: Works seamlessly with ActiveRecord models

Installation

Add this line to your application's Gemfile:

gem 'active-query'

And then execute:

bundle install

Usage

Basic Query Object

Create a query object by including ActiveQuery::Base and defining queries with the query method:

class UserQuery
  include ActiveQuery::Base

  # The model is automatically inferred from the class name (User)
  # Or explicitly set it:
  model_name 'User'

  # Simple query without arguments
  query :active, 'Returns all active users', -> { scope.where(active: true) }

  # Query with arguments and type validation
  query :by_email, 'Find users by email address',
    { email: { type: String } },
    -> (email:) { scope.where(email: email) }
end

Using Query Objects

# Execute queries
active_users = UserQuery.active
user = UserQuery.by_email(email: 'john@example.com')

# Chain with ActiveRecord methods
recent_active_users = UserQuery.active.where('created_at > ?', 1.week.ago)

Argument Types and Validation

ActiveQuery supports several argument types with automatic validation:

class ProductQuery
  include ActiveQuery::Base

  query :filter_products, 'Filter products by various criteria',
    {
      name: { type: String },
      price: { type: Float },
      quantity: { type: Integer },
      available: { type: Boolean }
    },
    -> (name:, price:, quantity:, available:) {
      scope.where(name: name)
           .where(price: price)
           .where(quantity: quantity)
           .where(available: available)
    }
end

# Usage with type validation
ProductQuery.filter_products(
  name: 'Widget',
  price: 19.99,
  quantity: 10,
  available: true
)

# This will raise ArgumentError due to type mismatch
ProductQuery.filter_products(name: 123, price: 'invalid', quantity: true, available: 'yes')

Optional Arguments and Defaults

class OrderQuery
  include ActiveQuery::Base

  query :search_orders, 'Search orders with optional filters',
    {
      status: { type: String },
      paid: { type: Boolean, default: true },
      customer_name: { type: String, optional: true }
    },
    -> (status:, paid:, customer_name:) {
      scope.where(status: status)
           .where(paid: paid)
           .if(customer_name, -> { where('customer_name LIKE ?', "%#{customer_name}%") })
    }
end

# Usage - customer_name is optional, paid defaults to true
OrderQuery.search_orders(status: 'pending')
OrderQuery.search_orders(status: 'completed', customer_name: 'John')
OrderQuery.search_orders(status: 'pending', paid: false, customer_name: 'Jane')

Conditional Query Logic

Use if and unless methods for conditional query building:

class UserQuery
  include ActiveQuery::Base

  query :search_users, 'Search users with conditional filters',
    {
      name: { type: String, optional: true },
      active: { type: Boolean, optional: true }
    },
    -> (name:, active:) {
      scope.if(name.present?, -> { where('name LIKE ?', "%#{name}%") })
           .if(active == true, -> { where(active: true) })
    }

  query :filter_unless_admin, 'Filter users unless they are admin',
    {
      role: { type: String, optional: true }
    },
    -> (role:) {
      scope.unless(role == 'admin', -> { where.not(role: 'admin') })
    }
end

Extended Query Operations

ActiveQuery provides additional query operations beyond standard ActiveRecord:

class ProductQuery
  include ActiveQuery::Base

  # Comparison operations
  query :expensive_products, 'Products above price threshold', -> { scope.gt(:price, 100) }
  query :affordable_products, 'Products within budget', -> { scope.lteq(:price, 50) }

  # Text search operations
  query :search_by_name, 'Search products by name pattern', -> { scope.like(:name, 'Phone') }
  query :products_starting_with, 'Products starting with prefix', -> { scope.start_like(:name, 'iPhone') }
  query :products_ending_with, 'Products ending with suffix', -> { scope.end_like(:name, 'Pro') }

  # Dynamic filtering
  query :by_price_range, 'Filter by price range',
    { min_price: { type: Float }, max_price: { type: Float } },
    -> (min_price:, max_price:) {
      scope.gteq(:price, min_price)
           .lteq(:price, max_price)
    }
end

Available operations:

  • gt(column, value) - greater than
  • gteq(column, value) - greater than or equal
  • lt(column, value) - less than
  • lteq(column, value) - less than or equal
  • like(column, value) - contains pattern (wraps with %)
  • start_like(column, value) - starts with pattern
  • end_like(column, value) - ends with pattern

Custom Scopes

Define reusable scopes within your query objects:

class UserQuery
  include ActiveQuery::Base
  include ActiveQuery::Scopes

  # Define custom scopes
  module Scopes
    include ActiveQuery::Scopes

    scope :recent, -> { where('created_at > ?', 1.month.ago) }
    scope :by_role, -> (role:) { where(role: role) }
  end

  # Use scopes in queries
  query :recent_admins, 'Find recent admin users', -> { scope.recent.by_role(role: 'admin') }

  query :count_recent, 'Count recent users', -> { scope.recent.count }
end

Resolver Pattern

For complex query logic, use the resolver pattern to keep your query objects clean:

# Define a resolver
class UserStatsResolver < ActiveQuery::Resolver
  def resolve(min_orders: 5)
    scope.joins(:orders)
         .group('users.id')
         .having('COUNT(orders.id) >= ?', min_orders)
         .select('users.*, COUNT(orders.id) as order_count')
  end
end

# Use resolver in query object
class UserQuery
  include ActiveQuery::Base

  # Resolver without arguments
  query :high_value_users, 'Users with many orders',
    resolver: UserStatsResolver

  # Resolver with arguments
  query :users_with_orders, 'Users with minimum order count',
    { min_orders: { type: Integer } },
    resolver: UserStatsResolver
end

# Usage
UserQuery.high_value_users
UserQuery.users_with_orders(min_orders: 10)

Query Introspection

Query objects provide metadata about available queries:

class UserQuery
  include ActiveQuery::Base

  query :active, 'Find active users', -> { scope.where(active: true) }
  query :by_name, 'Find by name', { name: { type: String } }, -> (name:) { scope.where(name: name) }
end

# Get all available queries
UserQuery.queries
# => [
#   { name: :active, description: "Find active users", args_def: {} },
#   { name: :by_name, description: "Find by name", args_def: { name: { type: String } } }
# ]

Error Handling

ActiveQuery provides clear error messages for common mistakes:

# Missing required arguments
UserQuery.by_email
# => ArgumentError: Params missing: [:email]

# Wrong argument type
UserQuery.by_email(email: 123)
# => ArgumentError: :email must be of type String

# Unknown arguments
UserQuery.by_email(email: 'test@example.com', invalid_param: 'value')
# => ArgumentError: Unknown params: [:invalid_param]

# Optional and default together (validation error)
query :invalid_query, 'This will fail',
  { param: { type: String, optional: true, default: 'value' } },
  -> (param:) { scope }
# => ArgumentError: Optional and Default params can't be present together: [:param]

Integration with Rails

ActiveQuery works seamlessly with Rails applications:

# app/queries/user_query.rb
class UserQuery
  include ActiveQuery::Base

  query :active, 'Active users', -> { scope.where(active: true) }
  query :by_role, 'Users by role', { role: { type: String } }, -> (role:) { scope.where(role: role) }
end

# In controllers
class UsersController < ApplicationController
  def index
    @users = UserQuery.active
  end

  def admins
    @admins = UserQuery.by_role(role: 'admin')
  end
end

# In views or anywhere else
<%= UserQuery.active.count %> active users

Requirements

  • Ruby >= 2.6.0
  • ActiveRecord >= 6.1
  • ActiveSupport >= 6.1

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/matiasasis/active-query. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the ActiveQuery project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.