Typical Sort
Predictable, allowlisted sorting for Rails controllers.
typical_sort is intentionally small. It does one thing: turns request sort params into safe ActiveRecord ordering.
Installation
gem "typical_sort"
Usage
class PatientsController < ApplicationController
include TypicalSort
typical_sort do
default :created_at, :desc
sort :created_at
sort :last_name
sort "account.name"
sort "insurance_plans.name", aggregate: :directional
end
def index
@patients = sort_records(Patient.all)
end
end
Supported params:
?sort=created_at&sort_dir=desc
?sort=-created_at
?sort=account.name
?sort=insurance_plans.name&sort_dir=asc
Only declared sorts are allowed. Unknown request sort params are ignored by default, or raised as TypicalSort::InvalidSort when configured with invalid_sort = :raise.
Base and association sorting
Declare base-table columns with symbols:
sort :last_name
sort :created_at
Declare one-level association paths with strings:
sort "account.name"
Association support:
belongs_tohas_onehas_many, with an aggregatehas_many :through, with an aggregatehas_and_belongs_to_many, with an aggregate- polymorphic
has_many, with an aggregate - STI associations using normal Rails reflections
Polymorphic belongs_to paths are not supported because there is no single target table to order by. Nested association paths such as "account.organization.name" are also not supported.
Aggregate sorting
Collection associations need one sortable value per parent record. Declare an aggregate explicitly:
sort "insurance_plans.name", aggregate: :directional
sort "payments.amount_cents", aggregate: :sum
sort "comments.id", aggregate: :count
Supported aggregate values:
:directional—MINfor ascending,MAXfor descending:min:max:sum:avg:count
Scope sorting
Use scope: true when a sort needs custom ordering logic:
class Book < ApplicationRecord
scope :by_publication, ->(dir) {
order(year_published: dir).order(author_name: :asc)
}
end
class BooksController < ApplicationController
include TypicalSort
typical_sort do
sort :by_publication, scope: true
end
end
Scope sorts always receive the resolved direction as the first argument: :asc or :desc.
Pagination
Apply sort_records before paginating so the database sorts the full filtered relation, then the pagination library applies LIMIT / OFFSET to the sorted relation.
Pagy
class PatientsController < ApplicationController
include Pagy::Backend
include TypicalSort
typical_sort do
default :created_at, :desc
sort :created_at
sort :last_name
sort "insurance_plans.name", aggregate: :directional
end
def index
patients = Patient.where(active: true)
patients = sort_records(patients)
@pagy, @patients = pagy(patients)
end
end
Kaminari
class PatientsController < ApplicationController
include TypicalSort
typical_sort do
default :created_at, :desc
sort :created_at
sort :last_name
sort "insurance_plans.name", aggregate: :directional
end
def index
@patients = Patient
.where(active: true)
.then { |records| sort_records(records) }
.page(params[:page])
.per(25)
end
end
Avoid paginating first and sorting second:
# Avoid: sorts only after the relation is already constrained by pagination.
sort_records(Patient.page(params[:page]))
Configuration
TypicalSort.configure do |config|
config.sort_param = :sort
config.direction_param = :sort_dir
config.default_direction = :asc
config.invalid_sort = :ignore # or :raise
config.tie_breaker = :primary_key
config.nulls = {
asc: :last,
desc: :first
}
end
nulls values must be :first or :last.
Development
bundle install
bundle exec appraisal install
bundle exec appraisal rspec
bundle exec standardrb
Releases
Releases are driven by git tags. The version lives in lib/typical_sort/version.rb, and the gemspec reads TypicalSort::VERSION.
bundle install
bin/release patch # or: minor, major
To publish an RC for a specific version:
bin/release rc 1.2.0
This sets the version to 1.2.0-rc and tags it as v1.2.0-rc.
Prereleases can also use bump's native pre increment:
bin/release pre
pre cycles prerelease labels in order: alpha, beta, rc, then the final version. For example, 1.2.0-rc becomes 1.2.0, tagged as v1.2.0.
bin/release uses bump, commits the version file, creates a v<version> tag, pushes the branch, and pushes the tag.
GitHub Actions publishes only when a v* tag is pushed. The publish workflow builds the gem and pushes it to RubyGems with RUBYGEMS_API_KEY.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/typical_sort.
License
The gem is available as open source under the terms of the MIT License.
Built by Apsis
typical_sort was built by Apsis Labs. We love sharing what we build! Check out our other libraries on Github, and if you like our work you can hire us to build your vision.
