Skip to content

Commit 79c6f46

Browse files
committed
add cursor based pagination
1 parent 18eccd6 commit 79c6f46

18 files changed

+808
-6
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ script: bundle exec rspec
77
env:
88
- PAGINATOR=kaminari
99
- PAGINATOR=will_paginate
10+
- PAGINATOR=cursor

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
1. Fork it
44
2. Create your feature branch (`git checkout -b my-new-feature`)
55
3. Commit your changes and tests (`git commit -am 'Add some feature'`)
6-
4. Run the tests (`PAGINATOR=kaminari bundle exec rspec; PAGINATOR=will_paginate bundle exec rspec`)
6+
4. Run the tests (`PAGINATOR=kaminari bundle exec rspec; PAGINATOR=will_paginate bundle exec rspec; PAGINATOR=cursor bundle exec rspec`)
77
5. Push to the branch (`git push origin my-new-feature`)
88
6. Create a new Pull Request

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,32 @@ class API::ApplicationController < ActionController::Base
113113
end
114114
```
115115

116+
### Cursor based pagination
117+
118+
In brief, it's really great in case of API when your entities create/destroy frequently.
119+
For more information about subject please follow
120+
[https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination](https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination)
121+
122+
Current implementation based on Kaminari and compatible with it model scoped config options.
123+
You can use it independently of Kaminari or WillPaginate.
124+
125+
Just use `cursor_paginate` method instead of `pagination`:
126+
127+
def cast
128+
actors = Movie.find(params[:id]).actors
129+
cursor_paginate json: actors, per_page: 10
130+
end
131+
132+
You can configure the following default values by overriding these values using `Cursor.configure` method.
133+
134+
default_per_page # 25 by default
135+
max_per_page # nil by default
136+
137+
Btw you can use cursor pagination as standalone feature:
138+
139+
movies = Movie.cursor_page(after: 10).per(10) # Get 10 movies where id > 10
140+
movies = Movie.cursor_page(before: 51).per(10) # Get 10 moview where id < 51
141+
116142
## Grape
117143

118144
With Grape, `paginate` is used to declare that your endpoint takes a `:page` and `:per_page` param. You can also directly specify a `:max_per_page` that users aren't allowed to go over. Then, inside your API endpoint, it simply takes your collection:
@@ -158,6 +184,20 @@ Per-Page: 10
158184
# ...
159185
```
160186

187+
And example for cursor based pagination:
188+
189+
```bash
190+
$ curl --include 'https://localhost:3000/movies?after=60'
191+
HTTP/1.1 200 OK
192+
Link: <http://localhost:3000/movies>; rel="first",
193+
<http://localhost:3000/movies?after=90>; rel="last",
194+
<http://localhost:3000/movies?after=70>; rel="next",
195+
<http://localhost:3000/movies?before=61>; rel="prev"
196+
Total: 100
197+
Per-Page: 10
198+
```
199+
200+
161201
## A Note on Kaminari and WillPaginate
162202

163203
api-pagination requires either Kaminari or WillPaginate in order to function, but some users may find themselves in situations where their application includes both. For example, you may have included [ActiveAdmin][activeadmin] (which uses Kaminari for pagination) and WillPaginate to do your own pagination. While it's suggested that you remove one paginator gem or the other, if you're unable to do so, you _must_ configure api-pagination explicitly:

api-pagination.gemspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@ Gem::Specification.new do |s|
2020
s.add_development_dependency 'grape', '>= 0.10.0'
2121
s.add_development_dependency 'railties', '>= 3.0.0'
2222
s.add_development_dependency 'actionpack', '>= 3.0.0'
23+
s.add_development_dependency 'activerecord', '>= 3.0.0'
2324
s.add_development_dependency 'sequel', '>= 4.9.0'
25+
s.add_development_dependency 'pry'
26+
s.add_development_dependency 'database_cleaner'
2427
end

lib/api-pagination/configuration.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ def paginator=(paginator)
5656
use_kaminari
5757
when :will_paginate
5858
use_will_paginate
59+
when :cursor
60+
use_cursor_paginator
5961
else
6062
raise StandardError, "Unknown paginator: #{paginator}"
6163
end
@@ -103,6 +105,10 @@ def last_page?() !next_page end
103105

104106
@paginator = :will_paginate
105107
end
108+
109+
def use_cursor_paginator
110+
@paginator = :cursor
111+
end
106112
end
107113

108114
class << self

lib/api-pagination/hooks.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ def self.rails_parent_controller
1919
ActiveSupport.on_load(:action_controller) do
2020
ApiPagination::Hooks.rails_parent_controller.send(:include, Rails::Pagination)
2121
end
22+
23+
ActiveSupport.on_load(:active_record) do
24+
require_relative '../cursor/active_record_extension'
25+
::ActiveRecord::Base.send :include, Cursor::ActiveRecordExtension
26+
end
2227
end
2328

2429
begin; require 'grape'; rescue LoadError; end

lib/cursor/active_record_extension.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'cursor/active_record_model_extension'
2+
3+
module Cursor
4+
module ActiveRecordExtension
5+
extend ActiveSupport::Concern
6+
7+
module ClassMethods
8+
# Future subclasses will pick up the model extension
9+
def inherited(kls) #:nodoc:
10+
super
11+
kls.send(:include, Cursor::ActiveRecordModelExtension) if kls.superclass == ::ActiveRecord::Base
12+
end
13+
end
14+
15+
included do
16+
# Existing subclasses pick up the model extension as well
17+
self.descendants.each do |kls|
18+
kls.send(:include, Cursor::ActiveRecordModelExtension) if kls.superclass == ::ActiveRecord::Base
19+
end
20+
end
21+
end
22+
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require_relative 'config'
2+
require_relative 'configuration_methods'
3+
require_relative 'page_scope_methods'
4+
5+
module Cursor
6+
module ActiveRecordModelExtension
7+
extend ActiveSupport::Concern
8+
9+
class_methods do
10+
cattr_accessor :total_count
11+
end
12+
13+
included do
14+
self.send(:include, Cursor::ConfigurationMethods)
15+
16+
def self.cursor_page(options = {})
17+
(options || {}).to_hash.symbolize_keys!
18+
options[:direction] = options.keys.include?(:after) ? :after : :before
19+
20+
cursor_id = options[options[:direction]]
21+
self.total_count = self.count
22+
on_cursor(cursor_id, options[:direction]).
23+
in_direction(options[:direction]).
24+
limit(options[:per_page] || default_per_page).
25+
extending(Cursor::PageScopeMethods)
26+
end
27+
28+
def self.on_cursor(cursor_id, direction)
29+
if cursor_id.nil?
30+
where(nil)
31+
else
32+
where(["#{self.table_name}.id #{direction == :after ? '>' : '<'} ?", cursor_id])
33+
end
34+
end
35+
36+
def self.in_direction(direction)
37+
reorder("#{self.table_name}.id #{direction == :after ? 'ASC' : 'DESC'}")
38+
end
39+
end
40+
end
41+
end

lib/cursor/config.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require 'active_support/configurable'
2+
3+
module Cursor
4+
# Configures global settings for Divination
5+
# Cursor.configure do |config|
6+
# config.default_per_page = 10
7+
# end
8+
def self.configure(&block)
9+
yield @config ||= Cursor::Configuration.new
10+
end
11+
12+
# Global settings for Cursor
13+
def self.config
14+
@config
15+
end
16+
17+
class Configuration #:nodoc:
18+
include ActiveSupport::Configurable
19+
config_accessor :default_per_page
20+
config_accessor :max_per_page
21+
22+
def param_name
23+
config.param_name.respond_to?(:call) ? config.param_name.call : config.param_name
24+
end
25+
end
26+
27+
configure do |config|
28+
config.default_per_page = 25
29+
config.max_per_page = nil
30+
end
31+
end

lib/cursor/configuration_methods.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module Cursor
2+
module ConfigurationMethods
3+
extend ActiveSupport::Concern
4+
5+
module ClassMethods
6+
# Overrides the default +per_page+ value per model
7+
# class Article < ActiveRecord::Base
8+
# paginates_per 10
9+
# end
10+
def paginates_per(val)
11+
@_default_per_page = val
12+
end
13+
14+
# This model's default +per_page+ value
15+
# returns +default_per_page+ value unless explicitly overridden via <tt>paginates_per</tt>
16+
def default_per_page
17+
(defined?(@_default_per_page) && @_default_per_page) || Cursor.config.default_per_page
18+
end
19+
20+
# Overrides the max +per_page+ value per model
21+
# class Article < ActiveRecord::Base
22+
# max_paginates_per 100
23+
# end
24+
def max_paginates_per(val)
25+
@_max_per_page = val
26+
end
27+
28+
# This model's max +per_page+ value
29+
# returns +max_per_page+ value unless explicitly overridden via <tt>max_paginates_per</tt>
30+
def max_per_page
31+
(defined?(@_max_per_page) && @_max_per_page) || Cursor.config.max_per_page
32+
end
33+
34+
end
35+
end
36+
end

0 commit comments

Comments
 (0)