rails_cursor_pagination 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92ff9fc0d88af01bbcee5b6bc749790494b476fe425492f799a1ebd0e4a35cf7
4
- data.tar.gz: 421e730132cc775c717e10bfb05309a98419d6096166f24db631390cfb4792cb
3
+ metadata.gz: 2410928079b14757601fdbb84b8c516a99070f5eee181ab464beeeeb00f302b6
4
+ data.tar.gz: 7f7e5a25d3977ad18964d35d0e2715978c57ccfdfe8624d229bd275deeb1b542
5
5
  SHA512:
6
- metadata.gz: 653cbdf05a63e16adab8f991dcf9fa90b0799424174c8b8838795f2f8cccdb89cd823e7a5708e485a8113e2af3ea6362c8a2696bfdac783ac30b8108795df841
7
- data.tar.gz: 6307cfb673d4d18ed4dff27efdc5de2f042cfeb09332d23cb7813ff7bf79ed761b219b7486330ff924dcbe624325b41a75a71955312dcd30992a84299ba5193c
6
+ metadata.gz: 349ba3f27691109cc3e374dbc0c8340163b8af0b452f158e806f859b7b88e4f6ba7b88bb9a5b9a3c9080ccf53d00c100ba7e166f4b0aec2e2fa0c300591b7ff5
7
+ data.tar.gz: 802a9c37b80c59dc52c747d6727afa48650572448294114a1f5b70c6092a72d272beb51eeda50b13d2b009598396688eff4969e1d44f13bfa09f7a2f7097c490
data/CHANGELOG.md CHANGED
@@ -14,6 +14,33 @@ 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.4.0] - 2023-10-06
18
+
19
+ ### Changed
20
+ - **Breaking change:** Raised minimum required Ruby version to 2.7
21
+ - **Breaking change:** Raised minimum required `activerecord` version to 6.0
22
+
23
+ ### Added
24
+ - Test against Ruby version 3.2
25
+
26
+ ### Fixed
27
+ - **Breaking change:** Ensure timestamp `order_by` fields (like `created_at`) will paginate results by honoring timestamp order down to microsecond resolution on comparison. This was done by changing the cursor logic for timestamp fields, which means that the cursors strings change from version 0.3.0 to 0.4.0 and old cursors cannot be decoded by the new gem version anymore.
28
+
29
+ ## [0.3.0] - 2022-07-08
30
+
31
+ ### Added
32
+ - Add a `limit` param to paginator that can be used instead either `first` or `last`
33
+ - Add a `max_page_size` to the configuration, allowing to set a global limit to the page size (non overridable): Default `nil`
34
+ - Support explicitly requesting all columns via `.select(*)` without re-including the requested column
35
+
36
+ ### Removed
37
+ - **Breaking change:** Drop support for Ruby 2.5 (EOL 2021-03-31)
38
+
39
+ ### Changed
40
+ - **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`.
41
+ - Refactor paginator cursor interactions into exposed `RailsCursorPagination::Cursor` class
42
+ - Require multi-factor-authentication to publish the gem on Rubygems
43
+
17
44
  ## [0.2.0] - 2021-04-19
18
45
 
19
46
  ### 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,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module RailsCursorPagination
6
+ # Cursor class that's used to uniquely identify a record and serialize and
7
+ # deserialize this cursor so that it can be used for pagination.
8
+ class Cursor
9
+ attr_reader :id, :order_field_value
10
+
11
+ class << self
12
+ # Generate a cursor for the given record and ordering field. The cursor
13
+ # encodes all the data required to then paginate based on it with the
14
+ # given ordering field.
15
+ #
16
+ # @param record [ActiveRecord]
17
+ # Model instance for which we want the cursor
18
+ # @param order_field [Symbol]
19
+ # Column or virtual column of the record that the relation is ordered by
20
+ # @return [Cursor]
21
+ def from_record(record:, order_field: :id)
22
+ new(id: record.id, order_field: order_field,
23
+ order_field_value: record[order_field])
24
+ end
25
+
26
+ # Decode the provided encoded cursor. Returns an instance of this
27
+ # +RailsCursorPagination::Cursor+ class containing either just the
28
+ # cursor's ID or in case of pagination on any other field, containing
29
+ # both the ID and the ordering field value.
30
+ #
31
+ # @param encoded_string [String]
32
+ # The encoded cursor
33
+ # @param order_field [Symbol]
34
+ # Optional. The column that is being ordered on in case it's not the ID
35
+ # column
36
+ # @return [RailsCursorPagination::Cursor]
37
+ def decode(encoded_string:, order_field: :id)
38
+ decoded = JSON.parse(Base64.strict_decode64(encoded_string))
39
+ if order_field == :id
40
+ if decoded.is_a?(Array)
41
+ raise InvalidCursorError,
42
+ "The given cursor `#{encoded_string}` was decoded as " \
43
+ "`#{decoded}` but could not be parsed"
44
+ end
45
+ new(id: decoded, order_field: :id)
46
+ else
47
+ unless decoded.is_a?(Array) && decoded.size == 2
48
+ raise InvalidCursorError,
49
+ "The given cursor `#{encoded_string}` was decoded as " \
50
+ "`#{decoded}` but could not be parsed"
51
+ end
52
+ new(id: decoded[1], order_field: order_field,
53
+ order_field_value: decoded[0])
54
+ end
55
+ rescue ArgumentError, JSON::ParserError
56
+ raise InvalidCursorError,
57
+ "The given cursor `#{encoded_string}` could not be decoded"
58
+ end
59
+ end
60
+
61
+ # Initializes the record
62
+ #
63
+ # @param id [Integer]
64
+ # The ID of the cursor record
65
+ # @param order_field [Symbol]
66
+ # The column or virtual column for ordering
67
+ # @param order_field_value [Object]
68
+ # Optional. The value that the +order_field+ of the record contains in
69
+ # case that the order field is not the ID
70
+ def initialize(id:, order_field: :id, order_field_value: nil)
71
+ @id = id
72
+ @order_field = order_field
73
+ @order_field_value = order_field_value
74
+
75
+ return if !custom_order_field? || !order_field_value.nil?
76
+
77
+ raise ParameterError, 'The `order_field` was set to ' \
78
+ "`#{@order_field.inspect}` but " \
79
+ 'no `order_field_value` was set'
80
+ end
81
+
82
+ # Generate an encoded string for this cursor. The cursor encodes all the
83
+ # data required to then paginate based on it with the given ordering field.
84
+ #
85
+ # If we only order by ID, the cursor doesn't need to include any other data.
86
+ # But if we order by any other field, the cursor needs to include both the
87
+ # value from this other field as well as the records ID to resolve the order
88
+ # of duplicates in the non-ID field.
89
+ #
90
+ # @return [String]
91
+ def encode
92
+ unencoded_cursor =
93
+ if custom_order_field?
94
+ [@order_field_value, @id]
95
+ else
96
+ @id
97
+ end
98
+ Base64.strict_encode64(unencoded_cursor.to_json)
99
+ end
100
+
101
+ private
102
+
103
+ # Returns true when the order has been overridden from the default (ID)
104
+ #
105
+ # @return [Boolean]
106
+ def custom_order_field?
107
+ @order_field != :id
108
+ end
109
+ end
110
+ 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_class.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,27 @@ 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"
377
+ memoize(:decoded_cursor) do
378
+ cursor_class.decode(encoded_string: @cursor, order_field: @order_field)
379
+ end
357
380
  end
358
381
 
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.
382
+ # Returns the appropriate class for the cursor based on the SQL type of the
383
+ # column used for ordering the relation.
362
384
  #
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
385
+ # @return [Class<RailsCursorPagination::Cursor>]
386
+ def cursor_class
387
+ order_field_type = @relation
388
+ .column_for_attribute(@order_field)
389
+ .sql_type_metadata
390
+ .type
369
391
 
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"
392
+ case order_field_type
393
+ when :datetime
394
+ TimestampCursor
395
+ else
396
+ Cursor
381
397
  end
382
-
383
- decoded_cursor.first
384
398
  end
385
399
 
386
400
  # Ensure that the relation has the ID column and any potential `order_by`
@@ -389,7 +403,8 @@ module RailsCursorPagination
389
403
  #
390
404
  # @return [ActiveRecord::Relation]
391
405
  def relation_with_cursor_fields
392
- return @relation if @relation.select_values.blank?
406
+ return @relation if @relation.select_values.blank? ||
407
+ @relation.select_values.include?('*')
393
408
 
394
409
  relation = @relation
395
410
 
@@ -453,15 +468,16 @@ module RailsCursorPagination
453
468
 
454
469
  unless custom_order_field?
455
470
  next sorted_relation.where "#{id_column} #{filter_operator} ?",
456
- decoded_cursor_id
471
+ decoded_cursor.id
457
472
  end
458
473
 
459
474
  sorted_relation
460
- .where("#{@order_field} #{filter_operator} ?", decoded_cursor_field)
475
+ .where("#{@order_field} #{filter_operator} ?",
476
+ decoded_cursor.order_field_value)
461
477
  .or(
462
478
  sorted_relation
463
- .where("#{@order_field} = ?", decoded_cursor_field)
464
- .where("#{id_column} #{filter_operator} ?", decoded_cursor_id)
479
+ .where("#{@order_field} = ?", decoded_cursor.order_field_value)
480
+ .where("#{id_column} #{filter_operator} ?", decoded_cursor.id)
465
481
  )
466
482
  end
467
483
  end
@@ -0,0 +1,84 @@
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
+ # This class expects the `order_field` of the record to be a timestamp and is
7
+ # to be used only when sorting a
8
+ class TimestampCursor < Cursor
9
+ class << self
10
+ # Decode the provided encoded cursor. Returns an instance of this
11
+ # `RailsCursorPagination::Cursor` class containing both the ID and the
12
+ # ordering field value. The ordering field is expected to be a timestamp
13
+ # and is always decoded in the UTC timezone.
14
+ #
15
+ # @param encoded_string [String]
16
+ # The encoded cursor
17
+ # @param order_field [Symbol]
18
+ # The column that is being ordered on. It needs to be a timestamp of a
19
+ # class that responds to `#strftime`.
20
+ # @raise [RailsCursorPagination::InvalidCursorError]
21
+ # In case the given `encoded_string` cannot be decoded properly
22
+ # @return [RailsCursorPagination::TimestampCursor]
23
+ # Instance of this class with a properly decoded timestamp cursor
24
+ def decode(encoded_string:, order_field:)
25
+ decoded = JSON.parse(Base64.strict_decode64(encoded_string))
26
+
27
+ new(
28
+ id: decoded[1],
29
+ order_field: order_field,
30
+ # Turn the order field value into a `Time` instance in UTC. A Rational
31
+ # number allows us to represent fractions of seconds, including the
32
+ # microseconds. In this way we can preserve the order of items with a
33
+ # microsecond precision.
34
+ # This also allows us to keep the size of the cursor small by using
35
+ # just a number instead of having to pass seconds and the fraction of
36
+ # seconds separately.
37
+ order_field_value: Time.at(decoded[0].to_r / (10**6)).utc
38
+ )
39
+ rescue ArgumentError, JSON::ParserError
40
+ raise InvalidCursorError,
41
+ "The given cursor `#{encoded_string}` " \
42
+ 'could not be decoded to a timestamp'
43
+ end
44
+ end
45
+
46
+ # Initializes the record. Overrides `Cursor`'s initializer making all params
47
+ # mandatory.
48
+ #
49
+ # @param id [Integer]
50
+ # The ID of the cursor record
51
+ # @param order_field [Symbol]
52
+ # The column or virtual column for ordering
53
+ # @param order_field_value [Object]
54
+ # The value that the +order_field+ of the record contains
55
+ def initialize(id:, order_field:, order_field_value:)
56
+ super id: id,
57
+ order_field: order_field,
58
+ order_field_value: order_field_value
59
+ end
60
+
61
+ # Encodes the cursor as an array containing the timestamp as microseconds
62
+ # from UNIX epoch and the id of the object
63
+ #
64
+ # @raise [RailsCursorPagination::ParameterError]
65
+ # The order field value needs to respond to `#strftime` to use the
66
+ # `TimestampCursor` class. Otherwise, a `ParameterError` is raised.
67
+ # @return [String]
68
+ def encode
69
+ unless @order_field_value.respond_to?(:strftime)
70
+ raise ParameterError,
71
+ "Could not encode #{@order_field} " \
72
+ "with value #{@order_field_value}." \
73
+ 'It does not respond to #strftime. Is it a timestamp?'
74
+ end
75
+
76
+ Base64.strict_encode64(
77
+ [
78
+ @order_field_value.strftime('%s%6N').to_i,
79
+ @id
80
+ ].to_json
81
+ )
82
+ end
83
+ end
84
+ 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.4.0'
5
5
  end
@@ -148,12 +148,24 @@
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
+
167
+ require_relative 'rails_cursor_pagination/timestamp_cursor'
168
+
157
169
  class << self
158
170
  # Allows to configure this gem. Currently supported configuration values
159
171
  # 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.4.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: 2023-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.0'
19
+ version: '6.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5.0'
26
+ version: '6.0'
27
27
  description: This library is an implementation of cursor pagination for ActiveRecord
28
28
  relations. Where a regular limit & offset pagination has issues with items that
29
29
  are being deleted from or added to the collection on previous pages, cursor pagination
@@ -40,7 +40,9 @@ 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
45
+ - lib/rails_cursor_pagination/timestamp_cursor.rb
44
46
  - lib/rails_cursor_pagination/version.rb
45
47
  homepage: https://github.com/xing/rails_cursor_pagination
46
48
  licenses:
@@ -49,6 +51,7 @@ metadata:
49
51
  homepage_uri: https://github.com/xing/rails_cursor_pagination
50
52
  source_code_uri: https://github.com/xing/rails_cursor_pagination
51
53
  changelog_uri: https://github.com/xing/rails_cursor_pagination/blob/master/CHANGELOG.md
54
+ rubygems_mfa_required: 'true'
52
55
  post_install_message:
53
56
  rdoc_options: []
54
57
  require_paths:
@@ -57,14 +60,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
57
60
  requirements:
58
61
  - - ">="
59
62
  - !ruby/object:Gem::Version
60
- version: 2.5.0
63
+ version: 2.7.0
61
64
  required_rubygems_version: !ruby/object:Gem::Requirement
62
65
  requirements:
63
66
  - - ">="
64
67
  - !ruby/object:Gem::Version
65
68
  version: '0'
66
69
  requirements: []
67
- rubygems_version: 3.1.4
70
+ rubygems_version: 3.2.33
68
71
  signing_key:
69
72
  specification_version: 4
70
73
  summary: Add cursor pagination to your ActiveRecord backed application.