cursor-paginate-r4 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +54 -0
  4. data/lib/cursor-paginate-r4.rb +8 -0
  5. data/lib/cursor-paginate-r4/active_record.rb +94 -0
  6. data/lib/cursor-paginate-r4/errors.rb +4 -0
  7. data/lib/cursor-paginate-r4/railtie.rb +9 -0
  8. data/lib/cursor-paginate-r4/version.rb +3 -0
  9. data/spec/active_record_spec.rb +240 -0
  10. data/spec/dummy/Rakefile +6 -0
  11. data/spec/dummy/app/assets/config/manifest.js +3 -0
  12. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  13. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  14. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  15. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  16. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  17. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  18. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  19. data/spec/dummy/app/jobs/application_job.rb +2 -0
  20. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  21. data/spec/dummy/app/models/application_record.rb +3 -0
  22. data/spec/dummy/app/models/post.rb +2 -0
  23. data/spec/dummy/app/views/layouts/application.html.erb +15 -0
  24. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  25. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  26. data/spec/dummy/bin/bundle +3 -0
  27. data/spec/dummy/bin/rails +4 -0
  28. data/spec/dummy/bin/rake +4 -0
  29. data/spec/dummy/bin/setup +36 -0
  30. data/spec/dummy/bin/update +31 -0
  31. data/spec/dummy/bin/yarn +11 -0
  32. data/spec/dummy/config.ru +5 -0
  33. data/spec/dummy/config/application.rb +30 -0
  34. data/spec/dummy/config/boot.rb +5 -0
  35. data/spec/dummy/config/cable.yml +10 -0
  36. data/spec/dummy/config/database.yml +25 -0
  37. data/spec/dummy/config/environment.rb +5 -0
  38. data/spec/dummy/config/environments/development.rb +61 -0
  39. data/spec/dummy/config/environments/production.rb +94 -0
  40. data/spec/dummy/config/environments/test.rb +46 -0
  41. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  42. data/spec/dummy/config/initializers/assets.rb +14 -0
  43. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  44. data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
  45. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  46. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  47. data/spec/dummy/config/initializers/inflections.rb +16 -0
  48. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  49. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  50. data/spec/dummy/config/locales/en.yml +33 -0
  51. data/spec/dummy/config/puma.rb +34 -0
  52. data/spec/dummy/config/routes.rb +3 -0
  53. data/spec/dummy/config/spring.rb +6 -0
  54. data/spec/dummy/config/storage.yml +34 -0
  55. data/spec/dummy/db/migrate/20190801041948_create_posts.rb +9 -0
  56. data/spec/dummy/db/schema.rb +21 -0
  57. data/spec/dummy/db/test.sqlite3 +0 -0
  58. data/spec/dummy/log/test.log +6420 -0
  59. data/spec/dummy/package.json +5 -0
  60. data/spec/dummy/public/404.html +67 -0
  61. data/spec/dummy/public/422.html +67 -0
  62. data/spec/dummy/public/500.html +66 -0
  63. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  64. data/spec/dummy/public/apple-touch-icon.png +0 -0
  65. data/spec/dummy/public/favicon.ico +0 -0
  66. data/spec/dummy/tmp/development_secret.txt +1 -0
  67. data/spec/rails_helper.rb +61 -0
  68. data/spec/spec_helper.rb +96 -0
  69. metadata +155 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0fea3272cce434833f7f628cfce3f3f1032a66091866d5abe6d986b30691cc04
4
+ data.tar.gz: 7b484eaea1f2229901ed90bf7865093ec06d5ccc345c2ed002985faab83f6e52
5
+ SHA512:
6
+ metadata.gz: 9986ba64dd47047291bad1763e5beceddd2b68b6a4212ae82dcb6dab789a8dba5aafbea05e0725402c637ec37dc4d9e12d4c4d10735c6db8aa1c1d836352b159
7
+ data.tar.gz: 4835a79fc8ac4ff36d2204a9337b5b4ef9b2f1cfcc400e51475e74255c2cb39202c257733442a82ad99c0c6f1c67fcba5f5c28b06318452b225028e8ff6e3666
@@ -0,0 +1,20 @@
1
+ Copyright 2019
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,54 @@
1
+ _Forked from [cursor-paginate](https://github.com/otoyo/cursor-paginate), Added Rails 4 support_
2
+
3
+ Cursor based pagination library for Rails.
4
+
5
+ ## Usage
6
+
7
+ ```ruby
8
+ # Get the newest 100 posts where id <= cursor
9
+ # If cursor is nil, get the newest 100 posts.
10
+ @posts = Post.before(id: params[:cursor]).limit(100)
11
+
12
+ # Get the oldest 100 posts where id >= cursor
13
+ # If cursor is nil, get the oldest 100 posts.
14
+ @posts = Post.after(id: params[:cursor]).limit(100)
15
+
16
+ # Also you can use other colum as cursor instead of id
17
+ @posts = Post.before(created_at: params[:cursor]).limit(100)
18
+
19
+ @posts.has_next? # => true/false
20
+ @posts.next_cursor # => value of cursor column/nil
21
+ ```
22
+
23
+ Note that if multiple data have the same cursor value (like `created_at` at the same time), they appear in duplicate.
24
+
25
+ ### In the template
26
+
27
+ If you use [Bootstrap 4.x](https://getbootstrap.com/docs/4.3/components/pagination/), pagers are expressed as below:
28
+
29
+ ```html
30
+ <nav aria-label="pagination">
31
+ <ul class="pagination">
32
+ <% if @posts.has_next? %>
33
+ <li class="page-item">
34
+ <%= link_to "Next &gt;", { controller: :posts, action: :index, cursor: @posts.next_cursor }, class: "page-link" %>
35
+ </li>
36
+ <% else %>
37
+ <li class="page-item disabled"><a href="#" class="page-link">Next &gt;</a></li>
38
+ <% end %>
39
+ </ul>
40
+ </nav>
41
+ ```
42
+
43
+ ## Installation
44
+ Add this line to your application's Gemfile:
45
+
46
+ ```ruby
47
+ gem 'cursor-paginate-r4'
48
+ ```
49
+
50
+ ## Contributing
51
+ Feel free to send PR.
52
+
53
+ ## License
54
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ module CursorPaginationR4
2
+ end
3
+
4
+ if defined?(Rails::Railtie)
5
+ require 'cursor-paginate-r4/railtie'
6
+ else
7
+ raise 'cursor-paginate-r4 is not available with your Rails version.'
8
+ end
@@ -0,0 +1,94 @@
1
+ require 'active_record'
2
+ require 'cursor-paginate-r4/errors'
3
+
4
+ module CursorPaginationR4
5
+ module ActiveRecord
6
+ module RelationMethods
7
+ attr_accessor :cursor_key
8
+
9
+ def before(options)
10
+ options = HashWithIndifferentAccess.new(options)
11
+
12
+ rel = if ::ActiveRecord::Relation === self
13
+ self
14
+ else
15
+ all
16
+ end
17
+
18
+ rel.cursor_key = rel.model.columns.map(&:name).find { |column| options.key? column }
19
+ raise CursorPaginationR4::InvalidColumnGiven unless rel.cursor_key
20
+
21
+ cursor = options[rel.cursor_key]
22
+
23
+ rel.where(cursor ? arel_table[rel.cursor_key].lteq(cursor) : nil).reorder(arel_table[rel.cursor_key].desc)
24
+ end
25
+
26
+ def after(options)
27
+ options = HashWithIndifferentAccess.new(options)
28
+
29
+ rel = if ::ActiveRecord::Relation === self
30
+ self
31
+ else
32
+ all
33
+ end
34
+
35
+ rel.cursor_key = rel.model.columns.map(&:name).find { |column| options.key? column }
36
+ raise CursorPaginationR4::InvalidColumnGiven unless rel.cursor_key
37
+
38
+ cursor = options[rel.cursor_key]
39
+
40
+ rel.where(cursor ? arel_table[rel.cursor_key].gteq(cursor) : nil).reorder(arel_table[rel.cursor_key].asc)
41
+ end
42
+
43
+ def has_next?
44
+ set_next if @has_next.nil?
45
+ @has_next
46
+ end
47
+
48
+ def next_cursor
49
+ set_next if @has_next.nil?
50
+ @next_cursor
51
+ end
52
+
53
+ # Override ActiveRecord::Relation#load
54
+ def load
55
+ return super unless cursor_key
56
+
57
+ rel = super
58
+ rel.set_next
59
+ rel
60
+ end
61
+
62
+ protected
63
+
64
+ def set_next
65
+ rel = self
66
+ raise CursorPaginationR4::LimitNotSet if rel.limit_value.nil?
67
+
68
+ excess = rel.dup.tap { |r| r.limit_value = r.limit_value + 1 }
69
+
70
+ if excess.size > rel.size
71
+ @has_next = true
72
+ @next_cursor = excess.last[cursor_key]
73
+ else
74
+ @has_next = false
75
+ @next_cursor = nil
76
+ end
77
+ end
78
+
79
+ ::ActiveRecord::Base.extend RelationMethods
80
+
81
+ klasses = [::ActiveRecord::Relation]
82
+ if defined? ::ActiveRecord::Associations::CollectionProxy
83
+ klasses << ::ActiveRecord::Associations::CollectionProxy
84
+ else
85
+ klasses << ::ActiveRecord::Associations::AssociationCollection
86
+ end
87
+
88
+ # # support pagination on associations and scopes
89
+ klasses.each do |klass|
90
+ klass.send(:include, RelationMethods)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,4 @@
1
+ module CursorPaginationR4
2
+ class InvalidColumnGiven < StandardError; end
3
+ class LimitNotSet < StandardError; end
4
+ end
@@ -0,0 +1,9 @@
1
+ module CursorPaginationR4
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'cursor-paginate-r4' do |app|
4
+ ActiveSupport.on_load :active_record do
5
+ require 'cursor-paginate-r4/active_record'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module CursorPaginationR4
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,240 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'RelationMethods' do
4
+ before do
5
+ 10.times do |i|
6
+ Post.create(created_at: Time.now + i)
7
+ end
8
+
9
+ posts[0, 3].each do |post|
10
+ post.is_published = true
11
+ post.save
12
+ end
13
+ end
14
+
15
+ after do
16
+ Post.delete_all
17
+ end
18
+
19
+ shared_examples_for 'next_cursor exists' do
20
+ describe '#has_next?' do
21
+ subject { posts.has_next? }
22
+
23
+ it { is_expected.to eq true }
24
+ end
25
+
26
+ describe 'next_cursor' do
27
+ subject { posts.next_cursor }
28
+
29
+ it { is_expected.to eq expected }
30
+ end
31
+ end
32
+
33
+ shared_examples_for 'no next_cursor' do
34
+ describe '#has_next?' do
35
+ subject { posts.has_next? }
36
+
37
+ it { is_expected.to eq false }
38
+ end
39
+
40
+ describe 'next_cursor' do
41
+ subject { posts.next_cursor }
42
+
43
+ it { is_expected.to be_nil }
44
+ end
45
+ end
46
+
47
+ describe '#before' do
48
+ context 'cursor is nil' do
49
+ let(:cursor) { nil }
50
+ let!(:posts) { Post.before(id: cursor).limit(limit) }
51
+
52
+ context 'limit is less than count of all' do
53
+ let(:limit) { 3 }
54
+ let(:expected) { Post.order('id desc')[limit].id }
55
+
56
+ it_behaves_like 'next_cursor exists'
57
+ end
58
+
59
+ context 'limit is equal to count of all' do
60
+ let(:limit) { Post.count }
61
+
62
+ it_behaves_like 'no next_cursor'
63
+ end
64
+ end
65
+
66
+ context 'id is given as cursor' do
67
+ let(:cursor_pos) { 3 }
68
+ let(:cursor) { Post.order('id desc')[cursor_pos].id }
69
+ let!(:posts) { Post.before(id: cursor).limit(limit) }
70
+
71
+ context 'limit is less than count of all' do
72
+ let(:limit) { 3 }
73
+ let(:expected) { Post.order('id desc')[limit + cursor_pos].id }
74
+
75
+ it_behaves_like 'next_cursor exists'
76
+ end
77
+
78
+ context 'limit is equal to count of all' do
79
+ let(:limit) { Post.count }
80
+
81
+ it_behaves_like 'no next_cursor'
82
+ end
83
+ end
84
+
85
+ context 'invalid column given' do
86
+ let(:cursor_pos) { 3 }
87
+ let(:posts) { Post.order('id desc') }
88
+ let(:cursor) { posts[cursor_pos].id }
89
+
90
+ subject { -> { Post.before(none: cursor) } }
91
+
92
+ it { is_expected.to raise_error(CursorPaginationR4::InvalidColumnGiven) }
93
+ end
94
+
95
+ context 'limit not set' do
96
+ let(:cursor_pos) { 3 }
97
+ let(:posts) { Post.order('id desc') }
98
+ let(:cursor) { posts[cursor_pos].id }
99
+
100
+ subject { -> { Post.before(id: cursor).has_next? } }
101
+
102
+ it { is_expected.to raise_error(CursorPaginationR4::LimitNotSet) }
103
+ end
104
+
105
+ context 'other condition is given' do
106
+ let(:cursor_pos) { 3 }
107
+ let(:cursor) { Post.where(is_published: false).order('id desc')[cursor_pos].id }
108
+ let!(:posts) { Post.where(is_published: false).before(id: cursor).limit(limit) }
109
+
110
+ context 'limit is less than count of all' do
111
+ let(:limit) { 3 }
112
+ let(:expected) { Post.where(is_published: false).order('id desc')[limit + cursor_pos].id }
113
+
114
+ it_behaves_like 'next_cursor exists'
115
+ end
116
+
117
+ context 'limit is equal to count of all' do
118
+ let(:limit) { Post.where(is_published: false).count }
119
+
120
+ it_behaves_like 'no next_cursor'
121
+ end
122
+ end
123
+
124
+ context 'created_at is given as cursor' do
125
+ let(:cursor_pos) { 3 }
126
+ let(:cursor) { Post.order('id desc')[cursor_pos].created_at }
127
+ let!(:posts) { Post.before(created_at: cursor).limit(limit) }
128
+
129
+ context 'limit is less than count of all' do
130
+ let(:limit) { 3 }
131
+ let(:expected) { Post.order('id desc')[limit + cursor_pos].created_at }
132
+
133
+ it_behaves_like 'next_cursor exists'
134
+ end
135
+
136
+ context 'limit is equal to count of all' do
137
+ let(:limit) { Post.count }
138
+
139
+ it_behaves_like 'no next_cursor'
140
+ end
141
+ end
142
+ end
143
+
144
+ describe '#after' do
145
+ context 'cursor is nil' do
146
+ let(:cursor) { nil }
147
+ let!(:posts) { Post.after(id: cursor).limit(limit) }
148
+
149
+ context 'limit is less than count of all' do
150
+ let(:limit) { 3 }
151
+ let(:expected) { Post.order('id asc')[limit].id }
152
+
153
+ it_behaves_like 'next_cursor exists'
154
+ end
155
+
156
+ context 'limit is equal to count of all' do
157
+ let(:limit) { Post.count }
158
+
159
+ it_behaves_like 'no next_cursor'
160
+ end
161
+ end
162
+
163
+ context 'id is given as cursor' do
164
+ let(:cursor_pos) { 3 }
165
+ let(:cursor) { Post.order('id asc')[cursor_pos].id }
166
+ let!(:posts) { Post.after(id: cursor).limit(limit) }
167
+
168
+ context 'limit is less than count of all' do
169
+ let(:limit) { 3 }
170
+ let(:expected) { Post.order('id asc')[limit + cursor_pos].id }
171
+
172
+ it_behaves_like 'next_cursor exists'
173
+ end
174
+
175
+ context 'limit is equal to count of all' do
176
+ let(:limit) { Post.count }
177
+
178
+ it_behaves_like 'no next_cursor'
179
+ end
180
+ end
181
+
182
+ context 'invalid column given' do
183
+ let(:cursor_pos) { 3 }
184
+ let(:posts) { Post.order('id desc') }
185
+ let(:cursor) { posts[cursor_pos].id }
186
+
187
+ subject { -> { Post.after(none: cursor) } }
188
+
189
+ it { is_expected.to raise_error(CursorPaginationR4::InvalidColumnGiven) }
190
+ end
191
+
192
+ context 'limit not set' do
193
+ let(:cursor_pos) { 3 }
194
+ let(:posts) { Post.order('id desc') }
195
+ let(:cursor) { posts[cursor_pos].id }
196
+
197
+ subject { -> { Post.after(id: cursor).has_next? } }
198
+
199
+ it { is_expected.to raise_error(CursorPaginationR4::LimitNotSet) }
200
+ end
201
+
202
+ context 'other condition is given' do
203
+ let(:cursor_pos) { 3 }
204
+ let(:cursor) { Post.where(is_published: false).order('id asc')[cursor_pos].id }
205
+ let!(:posts) { Post.where(is_published: false).after(id: cursor).limit(limit) }
206
+
207
+ context 'limit is less than count of all' do
208
+ let(:limit) { 3 }
209
+ let(:expected) { Post.where(is_published: false).order('id asc')[limit + cursor_pos].id }
210
+
211
+ it_behaves_like 'next_cursor exists'
212
+ end
213
+
214
+ context 'limit is equal to count of all' do
215
+ let(:limit) { Post.where(is_published: false).count }
216
+
217
+ it_behaves_like 'no next_cursor'
218
+ end
219
+ end
220
+
221
+ context 'created_at is given as cursor' do
222
+ let(:cursor_pos) { 3 }
223
+ let(:cursor) { Post.order('id asc')[cursor_pos].created_at }
224
+ let!(:posts) { Post.after(created_at: cursor).limit(limit) }
225
+
226
+ context 'limit is less than count of all' do
227
+ let(:limit) { 3 }
228
+ let(:expected) { Post.order('id asc')[limit + cursor_pos].created_at }
229
+
230
+ it_behaves_like 'next_cursor exists'
231
+ end
232
+
233
+ context 'limit is equal to count of all' do
234
+ let(:limit) { Post.count }
235
+
236
+ it_behaves_like 'no next_cursor'
237
+ end
238
+ end
239
+ end
240
+ end