No release in over a year
ActiveRecord extension for cursor based pagination.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 1.15
>= 0
~> 13.0
~> 3.0
~> 1.21

Runtime

>= 2.5
 Project Readme

Gem Version Tests

ActiverecordCursorPagination

ActiveRecord plugin for cursor based pagination using a serialized representation of the pages to paginate your content.

The main advantage to cursor based pagination over the traditional (limit & offset) is that the cursors are not impacted by changes to the query (i.e. new records or records that no longer fit the query conditions).

The advantage of ActiverecordCursorPagination over other gems is their is no requirement to define a row key (usually id) to sort the records. This allows for more complex queries to include joins or subqueries, and for ordering to also include table aliases or complex operations.

Motivation

I needed a cursor pagination method that was key agnostic and where I can order the records in any method I wish; including using complex queries or table aliases. This was especially important when building out user feeds.

Installation

Add this line to your application's Gemfile:

gem 'activerecord_cursor_pagination'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install activerecord_cursor_pagination

The cursor Basics

By default, ActiverecordCursorPagination defaults to 15 results per page.

# Imagine there are 60 *total* posts (at 10 results/page, that is 6 pages)
cursor = Posts.where(published: true)
  .order(published_at: :desc)
  .cursor(nil)

cursor.per_page         # => 10

# scoped to the whole query
cursor.scope_size       # => 60
cursor.scope_empty?     # => false
cursor.scope_any?       # => true
cursor.scope_one?       # => false
cursor.scope_many?      # => true

# scoped to the current page
cursor.size             # => 10
cursor.empty?           # => false
cursor.any?             # => true
cursor.one?             # => false
cursor.many?            # => true

# pagination
cursor.current_page     # => "serialized cursor..."
cursor.first_page?      # => true
cursor.last_page?       # => false
cursor.next_page?       # => true
cursor.next_page        # => "serialized cursor..."
cursor.previous_page?   # => false
cursor.previous_cursor  # => ""

To retrieve the next page of results, pass the next page cursor.

cursor = Posts.where(published: true)
  .order(published_at: :desc)
  .cursor("next page serialized cursor...")

cursor.per_page         # => 10

# scoped to the whole query
cursor.scope_size       # => 60
cursor.scope_empty?     # => false
cursor.scope_any?       # => true
cursor.scope_one?       # => false
cursor.scope_many?      # => true

# scoped to the current page
cursor.size             # => 10
cursor.empty?           # => false
cursor.any?             # => true
cursor.one?             # => false
cursor.many?            # => true

# pagination
cursor.current_page     # => "serialized cursor..."
cursor.first_page?      # => false
cursor.last_page?       # => false
cursor.next_page?       # => true
cursor.next_page        # => "serialized cursor..."
cursor.previous_page?   # => true
cursor.previous_cursor  # => "serialized cursor..."

You can iterate through the current page of results.

cursor.each { |record| /* do something */ }
cursor.each_with_index { |record, index| /* do something */ }
mapped = cursor.map { |record| /* do something */ }
mapped = cursor.map_with_index { |record, index| /* do something */ }

A custom number of results per page can be specified by passing the per option.

cursor = Posts.where(published: true)
  .order(published_at: :desc)
  .cursor(nil, per: 50)

PagerView Helpers

Lets image you have a pager view that displays one Post at a time and you have left and right errors to go to the next or previous record.

post = Post.find(10)

cursor = Posts.where(published: true)
  .order(published_at: :desc)
  .cursor(post, per: 1)

cursor.next_cursor_record       # => [Post] Next published post
cursor.previous_cursor_record   # => [Post] Previous published post

Make sure to set per to 1 or you will get a NotSingleRecordError

If no record can be found, next_cursor_record and previous_cursor_record will return nil.

Configuration

Configure ActiverecordCursorPagination using the setup method.

ActiverecordCursorPagination.setup do |config|
  config.secret_key = 'your super secret key'
  config.serializer = YourCustomSerializer
end

Custom Cursor Serializer

To create a custom cursor serializer, you need to override ActiverecordCursorPagination::Serializer. Call secret_key in your custom class to get the configured cursor key.

If you secure your database with external ids, make sure to encrypt the tokens so you don't expose the internal database ids.

For instance, to create a JWT serializer:

class JwtCursorSerializer < ActiverecordCursorPagination::Serializer
  def deserialize(str)
    data = JWT.decode str,
                      secret_key,
                      true,
                      { algorithm: 'HS256' }

    data.first.symbolize_keys
  end

  def serialize(hash)
    JWT.encode hash, secret_key,'HS256'
  end
end

Make sure to configure ActiverecordCursorPagination by setting the serializer configuration option with your new serializer.

Known Issues/Limitations

  • There is no known public method to call to get the order values. Currently calls order_values to get a list of all order values in the current query scope.
  • When using a sub query or CASE statement as an order value, you have to use single quote strings.

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 that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Testing

Run rake rspec to run all the tests or you can run:

  • rake rspec [path] to run all the tests in a given directory,
  • or rake rspec [file] to run a specific file.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/jefawks3/activerecord_cursor_pagination.

I hope that you will consider contributing to ActiverecordCursorPagination. You can contribute in many ways. For example, you might:

  • add documentation and “how-to” articles to the README or Wiki.
  • hack on ActiverecordCursorPagination itself by fixing bugs you've found in the GitHub Issue tracker or adding new features to ActiverecordCursorPagination.

When contributing to ActiverecordCursorPagination, we ask that you:

  • let me know what you plan in the GitHub Issue tracker so I can provide feedback.
  • provide tests and documentation whenever possible. It is very unlikely that I will accept new features or functionality into ActiverecordCursorPagination without the proper testing and documentation. When fixing a bug, provide a failing test case that your patch solves.
  • open a GitHub Pull Request with your patches and I will review your contribution and respond as quickly as possible.

Keep in mind that this is an open source project, and it may take me some time to get back to you. Your patience is very much appreciated.

License

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