Quo Core API

This section covers the core functionality of Quo, including:

  • Creating query objects
  • Working with relations and collections
  • Type-safe properties
  • Pagination
  • Query composition
  • Result transformation
  • Fluent API methods

Configuration

Quo provides several configuration options to customize its behavior.

Create an initializer to configure Quo:

# config/initializers/quo.rb
module Quo
  # Set custom base classes for your queries
  self.relation_backed_query_base_class = "ApplicationQuery"
  self.collection_backed_query_base_class = "ApplicationCollectionQuery"

  # Configure pagination defaults
  self.default_page_size = 25  # Default: 20
  self.max_page_size = 100     # Default: 200
end

Custom Base Classes

You can define your own base classes for queries to add application-specific functionality:

# app/queries/application_query.rb
class ApplicationQuery < Quo::RelationBackedQuery
  # Add common scopes or methods using the fluent API

  def with_status(status)
    with(where: {status: status})
  end
end

# app/queries/application_collection_query.rb
class ApplicationCollectionQuery < Quo::CollectionBackedQuery
  # Add common collection processing
end

Now all your queries can inherit from these custom base classes:

class UsersQuery < ApplicationQuery
  def query
    User.all
  end
end

# Use the custom methods
UsersQuery.new.with_status("active").results

Project Organization

Suggested directory structure:

app/
  queries/
    application_query.rb
    application_collection_query.rb
    users/
      active_users_query.rb
      by_role_query.rb
    posts/
      published_posts_query.rb
      recent_posts_query.rb

This organization keeps your query objects well-organized and easy to find.

Usage Examples

This page provides real-world examples of using Quo in your Rails application.

Basic Query Object

class ActiveUsersQuery < Quo::RelationBackedQuery
  def query
    User.where(active: true)
  end
end

# Usage
active_users = ActiveUsersQuery.new.results.to_a

Query with Properties

class UsersByRoleQuery < Quo::RelationBackedQuery
  prop :role, String
  prop :active_only, _Boolean, default: -> { true }

  def query
    scope = User.where(role: role)
    scope = scope.where(active: true) if active_only
    scope
  end
end

# Usage
admins = UsersByRoleQuery.new(role: "admin").results
all_moderators = UsersByRoleQuery.new(role: "moderator", active_only: false).results

Pagination

class PostsQuery < Quo::RelationBackedQuery
  prop :published_only, _Boolean, default: -> { true }

  def query
    scope = Post.order(created_at: :desc)
    scope = scope.where(published: true) if published_only
    scope
  end
end

# Usage with pagination
page1 = PostsQuery.new(page: 1, page_size: 20).results
page2 = PostsQuery.new(page: 2, page_size: 20).results

# Navigate between pages
query = PostsQuery.new(page: 1, page_size: 20)
next_query = query.next_page_query
prev_query = query.previous_page_query

Query Composition

class PublishedPostsQuery < Quo::RelationBackedQuery
  def query
    Post.where(published: true)
  end
end

class FeaturedPostsQuery < Quo::RelationBackedQuery
  def query
    Post.where(featured: true)
  end
end

# Compose queries
published_and_featured = PublishedPostsQuery.new + FeaturedPostsQuery.new
results = published_and_featured.results

Composition with Joins

class PostsByAuthorQuery < Quo::RelationBackedQuery
  prop :author_name, String

  def query
    Author.where("name LIKE ?", "%#{author_name}%")
  end
end

class PublishedPostsQuery < Quo::RelationBackedQuery
  def query
    Post.where(published: true)
  end
end

# Compose with explicit joins
posts = PublishedPostsQuery.new
  .merge(PostsByAuthorQuery.new(author_name: "John"), joins: :author)
  .results

Result Transformation

class UserPresenter
  attr_reader :user, :position

  def initialize(user, position: nil)
    @user = user
    @position = position
  end

  def formatted_name
    prefix = position ? "#{position + 1}. " : ""
    "#{prefix}#{user.first_name} #{user.last_name}"
  end
end

# Apply transformation
query = ActiveUsersQuery.new
  .transform { |user| UserPresenter.new(user) }

# With index parameter
query = ActiveUsersQuery.new
  .transform { |user, index| UserPresenter.new(user, position: index) }

query.results.each do |presenter|
  puts presenter.formatted_name
end

Collection-Backed Queries

class TopRatedItemsQuery < Quo::CollectionBackedQuery
  prop :minimum_rating, _Float(0..5.0), default: -> { 4.0 }

  def collection
    # Maybe from a cache or external API
    cached_items = Rails.cache.fetch("all_items") { Item.all.to_a }
    cached_items.select { |item| item.rating >= minimum_rating }
  end
end

# Usage
top_items = TopRatedItemsQuery.new(minimum_rating: 4.5).results

Fluent API Methods

For detailed information on the fluent API methods (where, order, includes, limit, etc.), see the API Reference.

query = PostsQuery.new
  .where(category: "Technology")
  .order(published_at: :desc)
  .includes(:author, :comments)
  .limit(10)

results = query.results

Testing with Helpers

Minitest

class UserQueryTest < ActiveSupport::TestCase
  include Quo::Minitest::Helpers

  test "returns active users" do
    users = [User.new(name: "Alice"), User.new(name: "Bob")]

    fake_query(ActiveUsersQuery, results: users) do
      result = ActiveUsersQuery.new.results.to_a
      assert_equal users, result
    end
  end
end

RSpec

RSpec.describe ActiveUsersQuery do
  include Quo::Rspec::Helpers

  it "returns active users" do
    users = [User.new(name: "Alice"), User.new(name: "Bob")]

    fake_query(ActiveUsersQuery, results: users) do
      result = ActiveUsersQuery.new.results.to_a
      expect(result).to eq(users)
    end
  end
end

Table of contents


Quo | Copyright © 2025. Licensed under the MIT License.