rails_cursor_pagination 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92ff9fc0d88af01bbcee5b6bc749790494b476fe425492f799a1ebd0e4a35cf7
4
- data.tar.gz: 421e730132cc775c717e10bfb05309a98419d6096166f24db631390cfb4792cb
3
+ metadata.gz: 43aad1bc560c58779869951edd254017bd3d15c0909022df0aeecbdc5c48bcef
4
+ data.tar.gz: 3ea26d6133869375d61644eebda2878dd6ccc65b906cbdc5bfa0294bfecd7a0c
5
5
  SHA512:
6
- metadata.gz: 653cbdf05a63e16adab8f991dcf9fa90b0799424174c8b8838795f2f8cccdb89cd823e7a5708e485a8113e2af3ea6362c8a2696bfdac783ac30b8108795df841
7
- data.tar.gz: 6307cfb673d4d18ed4dff27efdc5de2f042cfeb09332d23cb7813ff7bf79ed761b219b7486330ff924dcbe624325b41a75a71955312dcd30992a84299ba5193c
6
+ metadata.gz: dadfee2340eafb20eb0cd5d2e93f76544386554f7e0bf09935915816af72185a2eec6930270883fdd9870b90270a32677e364811b3914c809ffd38a4a839a296
7
+ data.tar.gz: ba8d85b9cbc57f546bf234f4d4653df7cfe1181cf78f05581c2f0fc4d6ed2a000d8adf39960346fec2b1074d66ab268b45c733d5c0b7e63d3830a54f12352a54
data/CHANGELOG.md CHANGED
@@ -14,6 +14,21 @@ These are the latest changes on the project's `master` branch that have not yet
14
14
  Follow the same format as previous releases by categorizing your feature into "Added", "Changed", "Deprecated", "Removed", "Fixed", or "Security".
15
15
  --->
16
16
 
17
+ ## [0.3.0] - 2022-07-08
18
+
19
+ ### Added
20
+ - Add a `limit` param to paginator that can be used instead either `first` or `last`
21
+ - Add a `max_page_size` to the configuration, allowing to set a global limit to the page size (non overridable): Default `nil`
22
+ - Support explicitly requesting all columns via `.select(*)` without re-including the requested column
23
+
24
+ ### Removed
25
+ - **Breaking change:** Drop support for Ruby 2.5 (EOL 2021-03-31)
26
+
27
+ ### Changed
28
+ - **Breaking change:** Remove nesting of `ParameterError` and `InvalidCursorError` errors, they are now directly nested under the main gem module. So they're now `RailsCursorPagination::ParameterError` and `RailsCursorPagination::InvalidCursorError`.
29
+ - Refactor paginator cursor interactions into exposed `RailsCursorPagination::Cursor` class
30
+ - Require multi-factor-authentication to publish the gem on Rubygems
31
+
17
32
  ## [0.2.0] - 2021-04-19
18
33
 
19
34
  ### Changed
data/README.md CHANGED
@@ -120,6 +120,21 @@ RailsCursorPagination::Paginator
120
120
  .fetch
121
121
  ```
122
122
 
123
+ Alternatively, you can use the `limit` column with either `after` or `before`.
124
+ This will behave like either `first` or `last` respectively and fetch X records.
125
+
126
+ ```ruby
127
+ RailsCursorPagination::Paginator
128
+ .new(posts, limit: 2, after: 'MTA=')
129
+ .fetch
130
+ ```
131
+
132
+ ```ruby
133
+ RailsCursorPagination::Paginator
134
+ .new(posts, limit: 2, before: 'MTA=')
135
+ .fetch
136
+ ```
137
+
123
138
  ### Ordering
124
139
 
125
140
  As said, this gem ignores any previous ordering added to the passed relation.
@@ -216,7 +231,15 @@ end
216
231
  ```
217
232
 
218
233
  This would set the default page size to 50.
219
-
234
+
235
+ You can also select a global `max_page_size` to prevent a client from requesting too large a page.
236
+
237
+ ```ruby
238
+ RailsCursorPagination.configure do |config|
239
+ config.max_page_size = 100
240
+ end
241
+ ```
242
+
220
243
  ### The passed relation
221
244
 
222
245
  The relation passed to the `RailsCursorPagination::Paginator` needs to be an instance of an `ActiveRecord::Relation`.
@@ -404,6 +427,20 @@ You can also run `bin/console` for an interactive prompt that will allow you to
404
427
  To install this gem onto your local machine, run `bundle exec rake install`.
405
428
  To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
406
429
 
430
+ ## Supported environments
431
+
432
+ This gem should run in any project that uses:
433
+ * Ruby
434
+ * `ActiveRecord`
435
+ * Postgres or MySQL
436
+
437
+ We aim to support all versions that are still actively maintained and extend support until one year past the version's EOL.
438
+ While we think it's important to stay up-to-date with versions and update as soon as an EOL is reached, we know that this is not always immediately possible.
439
+ This way, we hope to strike a balance between being usable by most projects without forcing them to upgrade, but also keeping the supported version combinations manageable.
440
+
441
+ This project is tested against different permutations of Ruby versions and DB versions, both Postgres and MySQL.
442
+ Please check the [test automation file under `./.github/workflows/test.yml`](.github/workflows/test.yml) to see all officially supported combinations.
443
+
407
444
  ## Contributing
408
445
 
409
446
  Bug reports and pull requests are welcome on GitHub at https://github.com/xing/rails_cursor_pagination.
@@ -10,12 +10,13 @@ module RailsCursorPagination
10
10
  #
11
11
  # RailsCursorPagination.configure do |config|
12
12
  # config.default_page_size = 42
13
+ # config.max_page_size = 100
13
14
  # end
14
15
  #
15
16
  class Configuration
16
17
  include Singleton
17
18
 
18
- attr_accessor :default_page_size
19
+ attr_accessor :default_page_size, :max_page_size
19
20
 
20
21
  # Ensure the default values are set on first initialization
21
22
  def initialize
@@ -25,6 +26,7 @@ module RailsCursorPagination
25
26
  # Reset all values to their defaults
26
27
  def reset!
27
28
  @default_page_size = 10
29
+ @max_page_size = nil
28
30
  end
29
31
  end
30
32
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsCursorPagination
4
+ # Cursor class that's used to uniquely identify a record and serialize and
5
+ # deserialize this cursor so that it can be used for pagination.
6
+ class Cursor
7
+ attr_reader :id, :order_field_value
8
+
9
+ class << self
10
+ # Generate a cursor for the given record and ordering field. The cursor
11
+ # encodes all the data required to then paginate based on it with the
12
+ # given ordering field.
13
+ #
14
+ # @param record [ActiveRecord]
15
+ # Model instance for which we want the cursor
16
+ # @param order_field [Symbol]
17
+ # Column or virtual column of the record that the relation is ordered by
18
+ # @return [Cursor]
19
+ def from_record(record:, order_field: :id)
20
+ new(id: record.id, order_field: order_field,
21
+ order_field_value: record[order_field])
22
+ end
23
+
24
+ # Decode the provided encoded cursor. Returns an instance of this
25
+ # +RailsCursorPagination::Cursor+ class containing either just the
26
+ # cursor's ID or in case of pagination on any other field, containing
27
+ # both the ID and the ordering field value.
28
+ #
29
+ # @param encoded_string [String]
30
+ # The encoded cursor
31
+ # @param order_field [Symbol]
32
+ # Optional. The column that is being ordered on in case it's not the ID
33
+ # column
34
+ # @return [RailsCursorPagination::Cursor]
35
+ def decode(encoded_string:, order_field: :id)
36
+ decoded = JSON.parse(Base64.strict_decode64(encoded_string))
37
+ if order_field == :id
38
+ if decoded.is_a?(Array)
39
+ raise InvalidCursorError,
40
+ "The given cursor `#{encoded_string}` was decoded as " \
41
+ "`#{decoded}` but could not be parsed"
42
+ end
43
+ new(id: decoded, order_field: :id)
44
+ else
45
+ unless decoded.is_a?(Array) && decoded.size == 2
46
+ raise InvalidCursorError,
47
+ "The given cursor `#{encoded_string}` was decoded as " \
48
+ "`#{decoded}` but could not be parsed"
49
+ end
50
+ new(id: decoded[1], order_field: order_field,
51
+ order_field_value: decoded[0])
52
+ end
53
+ rescue ArgumentError, JSON::ParserError
54
+ raise InvalidCursorError,
55
+ "The given cursor `#{encoded_string}` could not be decoded"
56
+ end
57
+ end
58
+
59
+ # Initializes the record
60
+ #
61
+ # @param id [Integer]
62
+ # The ID of the cursor record
63
+ # @param order_field [Symbol]
64
+ # The column or virtual column for ordering
65
+ # @param order_field_value [Object]
66
+ # Optional. The value that the +order_field+ of the record contains in
67
+ # case that the order field is not the ID
68
+ def initialize(id:, order_field: :id, order_field_value: nil)
69
+ @id = id
70
+ @order_field = order_field
71
+ @order_field_value = order_field_value
72
+
73
+ return if !custom_order_field? || !order_field_value.nil?
74
+
75
+ raise ParameterError, 'The `order_field` was set to ' \
76
+ "`#{@order_field.inspect}` but " \
77
+ 'no `order_field_value` was set'
78
+ end
79
+
80
+ # Generate an encoded string for this cursor. The cursor encodes all the
81
+ # data required to then paginate based on it with the given ordering field.
82
+ #
83
+ # If we only order by ID, the cursor doesn't need to include any other data.
84
+ # But if we order by any other field, the cursor needs to include both the
85
+ # value from this other field as well as the records ID to resolve the order
86
+ # of duplicates in the non-ID field.
87
+ #
88
+ # @return [String]
89
+ def encode
90
+ unencoded_cursor =
91
+ if custom_order_field?
92
+ [@order_field_value, @id]
93
+ else
94
+ @id
95
+ end
96
+ Base64.strict_encode64(unencoded_cursor.to_json)
97
+ end
98
+
99
+ private
100
+
101
+ # Returns true when the order has been overridden from the default (ID)
102
+ #
103
+ # @return [Boolean]
104
+ def custom_order_field?
105
+ @order_field != :id
106
+ end
107
+ end
108
+ end
@@ -11,18 +11,13 @@ module RailsCursorPagination
11
11
  # .fetch
12
12
  #
13
13
  class Paginator
14
- # Generic error that gets raised when invalid parameters are passed to the
15
- # Paginator initializer
16
- class ParameterError < Error; end
17
-
18
- # Error that gets raised if a cursor given as `before` or `after` parameter
19
- # cannot be properly parsed
20
- class InvalidCursorError < ParameterError; end
21
-
22
14
  # Create a new instance of the `RailsCursorPagination::Paginator`
23
15
  #
24
16
  # @param relation [ActiveRecord::Relation]
25
17
  # Relation that will be paginated.
18
+ # @param limit [Integer, nil]
19
+ # Number of records to return in pagination. Can be combined with either
20
+ # `after` or `before` as an alternative to `first` or `last`.
26
21
  # @param first [Integer, nil]
27
22
  # Number of records to return in a forward pagination. Can be combined
28
23
  # with `after`.
@@ -43,14 +38,15 @@ module RailsCursorPagination
43
38
  # @param order [Symbol, nil]
44
39
  # Ordering to apply, either `:asc` or `:desc`. Defaults to `:asc`.
45
40
  #
46
- # @raise [RailsCursorPagination::Paginator::ParameterError]
41
+ # @raise [RailsCursorPagination::ParameterError]
47
42
  # If any parameter is not valid
48
- def initialize(relation, first: nil, after: nil, last: nil, before: nil,
49
- order_by: nil, order: nil)
43
+ def initialize(relation, limit: nil, first: nil, after: nil, last: nil,
44
+ before: nil, order_by: nil, order: nil)
50
45
  order_by ||= :id
51
46
  order ||= :asc
52
47
 
53
- ensure_valid_params!(relation, first, after, last, before, order)
48
+ ensure_valid_params_values!(relation, order, limit, first, last)
49
+ ensure_valid_params_combinations!(first, last, limit, before, after)
54
50
 
55
51
  @order_field = order_by
56
52
  @order_direction = order
@@ -62,8 +58,14 @@ module RailsCursorPagination
62
58
  @page_size =
63
59
  first ||
64
60
  last ||
61
+ limit ||
65
62
  RailsCursorPagination::Configuration.instance.default_page_size
66
63
 
64
+ if Configuration.instance.max_page_size &&
65
+ Configuration.instance.max_page_size < @page_size
66
+ @page_size = Configuration.instance.max_page_size
67
+ end
68
+
67
69
  @memos = {}
68
70
  end
69
71
 
@@ -83,50 +85,79 @@ module RailsCursorPagination
83
85
 
84
86
  private
85
87
 
86
- # Ensure that the parameters of this service are valid. Otherwise raise
87
- # a `RailsCursorPagination::Paginator::ParameterError`.
88
+ # Ensure that the parameters of this service have valid values, otherwise
89
+ # raise a `RailsCursorPagination::ParameterError`.
88
90
  #
89
91
  # @param relation [ActiveRecord::Relation]
90
92
  # Relation that will be paginated.
93
+ # @param order [Symbol]
94
+ # Must be :asc or :desc
95
+ # @param limit [Integer, nil]
96
+ # Optional, must be positive
91
97
  # @param first [Integer, nil]
92
- # Optional, must be positive, cannot be combined with `last`
93
- # @param after [String, nil]
94
- # Optional, cannot be combined with `before`
98
+ # Optional, must be positive
95
99
  # @param last [Integer, nil]
96
- # Optional, must be positive, requires `before`, cannot be combined
97
- # with `first`
98
- # @param before [String, nil]
99
- # Optional, cannot be combined with `after`
100
- # @param order [Symbol]
101
- # Optional, must be :asc or :desc
100
+ # Optional, must be positive
101
+ # with `first` or `limit`
102
102
  #
103
- # @raise [RailsCursorPagination::Paginator::ParameterError]
103
+ # @raise [RailsCursorPagination::ParameterError]
104
104
  # If any parameter is not valid
105
- def ensure_valid_params!(relation, first, after, last, before, order)
105
+ def ensure_valid_params_values!(relation, order, limit, first, last)
106
106
  unless relation.is_a?(ActiveRecord::Relation)
107
107
  raise ParameterError,
108
- 'The first argument must be an ActiveRecord::Relation, but was '\
108
+ 'The first argument must be an ActiveRecord::Relation, but was ' \
109
109
  "the #{relation.class} `#{relation.inspect}`"
110
110
  end
111
111
  unless %i[asc desc].include?(order)
112
112
  raise ParameterError,
113
113
  "`order` must be either :asc or :desc, but was `#{order}`"
114
114
  end
115
+ if first.present? && first.negative?
116
+ raise ParameterError, "`first` cannot be negative, but was `#{first}`"
117
+ end
118
+ if last.present? && last.negative?
119
+ raise ParameterError, "`last` cannot be negative, but was `#{last}`"
120
+ end
121
+ if limit.present? && limit.negative?
122
+ raise ParameterError, "`limit` cannot be negative, but was `#{limit}`"
123
+ end
124
+
125
+ true
126
+ end
127
+
128
+ # Ensure that the parameters of this service are combined in a valid way.
129
+ # Otherwise raise a +RailsCursorPagination::ParameterError+.
130
+ #
131
+ # @param limit [Integer, nil]
132
+ # Optional, cannot be combined with `last` or `first`
133
+ # @param first [Integer, nil]
134
+ # Optional, cannot be combined with `last` or `limit`
135
+ # @param after [String, nil]
136
+ # Optional, cannot be combined with `before`
137
+ # @param last [Integer, nil]
138
+ # Optional, requires `before`, cannot be combined
139
+ # with `first` or `limit`
140
+ # @param before [String, nil]
141
+ # Optional, cannot be combined with `after`
142
+ #
143
+ # @raise [RailsCursorPagination::ParameterError]
144
+ # If parameters are combined in an invalid way
145
+ def ensure_valid_params_combinations!(first, last, limit, before, after)
115
146
  if first.present? && last.present?
116
147
  raise ParameterError, '`first` cannot be combined with `last`'
117
148
  end
149
+ if first.present? && limit.present?
150
+ raise ParameterError, '`limit` cannot be combined with `first`'
151
+ end
152
+ if last.present? && limit.present?
153
+ raise ParameterError, '`limit` cannot be combined with `last`'
154
+ end
118
155
  if before.present? && after.present?
119
156
  raise ParameterError, '`before` cannot be combined with `after`'
120
157
  end
121
158
  if last.present? && before.blank?
122
159
  raise ParameterError, '`last` must be combined with `before`'
123
160
  end
124
- if first.present? && first.negative?
125
- raise ParameterError, "`first` cannot be negative, but was `#{first}`"
126
- end
127
- if last.present? && last.negative?
128
- raise ParameterError, "`last` cannot be negative, but was `#{last}`"
129
- end
130
161
 
131
162
  true
132
163
  end
@@ -317,9 +348,9 @@ module RailsCursorPagination
317
348
  #
318
349
  # @return [Integer, String]
319
350
  def filter_value
320
- return decoded_cursor_id unless custom_order_field?
351
+ return decoded_cursor.id unless custom_order_field?
321
352
 
322
- "#{decoded_cursor_field}-#{decoded_cursor_id}"
353
+ "#{decoded_cursor.order_field_value}-#{decoded_cursor.id}"
323
354
  end
324
355
 
325
356
  # Generate a cursor for the given record and ordering field. The cursor
@@ -334,14 +365,7 @@ module RailsCursorPagination
334
365
  # @param record [ActiveRecord] Model instance for which we want the cursor
335
366
  # @return [String]
336
367
  def cursor_for_record(record)
337
- unencoded_cursor =
338
- if custom_order_field?
339
- [record[@order_field], record.id]
340
- else
341
- record.id
342
- end
343
-
344
- Base64.strict_encode64(unencoded_cursor.to_json)
368
+ Cursor.from_record(record: record, order_field: @order_field).encode
345
369
  end
346
370
 
347
371
  # Decode the provided cursor. Either just returns the cursor's ID or in case
@@ -350,37 +374,9 @@ module RailsCursorPagination
350
374
  #
351
375
  # @return [Integer, Array]
352
376
  def decoded_cursor
353
- memoize(:decoded_cursor) { JSON.parse(Base64.strict_decode64(@cursor)) }
354
- rescue ArgumentError, JSON::ParserError
355
- raise InvalidCursorError,
356
- "The given cursor `#{@cursor.inspect}` could not be decoded"
357
- end
358
-
359
- # Return the ID of the cursor's record. In case we use an ordering by ID,
360
- # this is all the data the cursor encodes. Otherwise, it's the second
361
- # element of the tuple encoded by the cursor.
362
- #
363
- # @return [Integer]
364
- def decoded_cursor_id
365
- return decoded_cursor unless decoded_cursor.is_a? Array
366
-
367
- decoded_cursor.last
368
- end
369
-
370
- # Return the value of the cursor's record's custom order field. Only exists
371
- # if the cursor was generated by a query with a custom order field.
372
- # Otherwise the cursor would only encode the ID and not be an array.
373
-
374
- # @raise [InvalidCursorError] in case the cursor is not a tuple
375
- # @return [Object]
376
- def decoded_cursor_field
377
- unless decoded_cursor.is_a? Array
378
- raise InvalidCursorError,
379
- "The given cursor `#{@cursor}` was decoded as "\
380
- "`#{decoded_cursor.inspect}` but could not be parsed"
377
+ memoize(:decoded_cursor) do
378
+ Cursor.decode(encoded_string: @cursor, order_field: @order_field)
381
379
  end
382
-
383
- decoded_cursor.first
384
380
  end
385
381
 
386
382
  # Ensure that the relation has the ID column and any potential `order_by`
@@ -389,7 +385,8 @@ module RailsCursorPagination
389
385
  #
390
386
  # @return [ActiveRecord::Relation]
391
387
  def relation_with_cursor_fields
392
- return @relation if @relation.select_values.blank?
388
+ return @relation if @relation.select_values.blank? ||
389
+ @relation.select_values.include?('*')
393
390
 
394
391
  relation = @relation
395
392
 
@@ -453,15 +450,16 @@ module RailsCursorPagination
453
450
 
454
451
  unless custom_order_field?
455
452
  next sorted_relation.where "#{id_column} #{filter_operator} ?",
456
- decoded_cursor_id
453
+ decoded_cursor.id
457
454
  end
458
455
 
459
456
  sorted_relation
460
- .where("#{@order_field} #{filter_operator} ?", decoded_cursor_field)
457
+ .where("#{@order_field} #{filter_operator} ?",
458
+ decoded_cursor.order_field_value)
461
459
  .or(
462
460
  sorted_relation
463
- .where("#{@order_field} = ?", decoded_cursor_field)
464
- .where("#{id_column} #{filter_operator} ?", decoded_cursor_id)
461
+ .where("#{@order_field} = ?", decoded_cursor.order_field_value)
462
+ .where("#{id_column} #{filter_operator} ?", decoded_cursor.id)
465
463
  )
466
464
  end
467
465
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsCursorPagination
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -148,12 +148,22 @@
148
148
  module RailsCursorPagination
149
149
  class Error < StandardError; end
150
150
 
151
+ # Generic error that gets raised when invalid parameters are passed to the
152
+ # pagination
153
+ class ParameterError < Error; end
154
+
155
+ # Error that gets raised if a cursor given as `before` or `after` cannot be
156
+ # properly parsed
157
+ class InvalidCursorError < ParameterError; end
158
+
151
159
  require_relative 'rails_cursor_pagination/version'
152
160
 
153
161
  require_relative 'rails_cursor_pagination/configuration'
154
162
 
155
163
  require_relative 'rails_cursor_pagination/paginator'
156
164
 
165
+ require_relative 'rails_cursor_pagination/cursor'
166
+
157
167
  class << self
158
168
  # Allows to configure this gem. Currently supported configuration values
159
169
  # are:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_cursor_pagination
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Fricke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-19 00:00:00.000000000 Z
11
+ date: 2022-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -40,6 +40,7 @@ files:
40
40
  - README.md
41
41
  - lib/rails_cursor_pagination.rb
42
42
  - lib/rails_cursor_pagination/configuration.rb
43
+ - lib/rails_cursor_pagination/cursor.rb
43
44
  - lib/rails_cursor_pagination/paginator.rb
44
45
  - lib/rails_cursor_pagination/version.rb
45
46
  homepage: https://github.com/xing/rails_cursor_pagination
@@ -49,6 +50,7 @@ metadata:
49
50
  homepage_uri: https://github.com/xing/rails_cursor_pagination
50
51
  source_code_uri: https://github.com/xing/rails_cursor_pagination
51
52
  changelog_uri: https://github.com/xing/rails_cursor_pagination/blob/master/CHANGELOG.md
53
+ rubygems_mfa_required: 'true'
52
54
  post_install_message:
53
55
  rdoc_options: []
54
56
  require_paths:
@@ -57,14 +59,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
57
59
  requirements:
58
60
  - - ">="
59
61
  - !ruby/object:Gem::Version
60
- version: 2.5.0
62
+ version: 2.6.0
61
63
  required_rubygems_version: !ruby/object:Gem::Requirement
62
64
  requirements:
63
65
  - - ">="
64
66
  - !ruby/object:Gem::Version
65
67
  version: '0'
66
68
  requirements: []
67
- rubygems_version: 3.1.4
69
+ rubygems_version: 3.1.6
68
70
  signing_key:
69
71
  specification_version: 4
70
72
  summary: Add cursor pagination to your ActiveRecord backed application.