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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +38 -1
- data/lib/rails_cursor_pagination/configuration.rb +3 -1
- data/lib/rails_cursor_pagination/cursor.rb +110 -0
- data/lib/rails_cursor_pagination/paginator.rb +89 -73
- data/lib/rails_cursor_pagination/timestamp_cursor.rb +84 -0
- data/lib/rails_cursor_pagination/version.rb +1 -1
- data/lib/rails_cursor_pagination.rb +12 -0
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2410928079b14757601fdbb84b8c516a99070f5eee181ab464beeeeb00f302b6
|
4
|
+
data.tar.gz: 7f7e5a25d3977ad18964d35d0e2715978c57ccfdfe8624d229bd275deeb1b542
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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::
|
41
|
+
# @raise [RailsCursorPagination::ParameterError]
|
47
42
|
# If any parameter is not valid
|
48
|
-
def initialize(relation,
|
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
|
-
|
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
|
87
|
-
# a `RailsCursorPagination::
|
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
|
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
|
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::
|
103
|
+
# @raise [RailsCursorPagination::ParameterError]
|
104
104
|
# If any parameter is not valid
|
105
|
-
def
|
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
|
351
|
+
return decoded_cursor.id unless custom_order_field?
|
321
352
|
|
322
|
-
"#{
|
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
|
-
|
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)
|
354
|
-
|
355
|
-
|
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
|
-
#
|
360
|
-
#
|
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 [
|
364
|
-
def
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
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
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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
|
-
|
471
|
+
decoded_cursor.id
|
457
472
|
end
|
458
473
|
|
459
474
|
sorted_relation
|
460
|
-
.where("#{@order_field} #{filter_operator} ?",
|
475
|
+
.where("#{@order_field} #{filter_operator} ?",
|
476
|
+
decoded_cursor.order_field_value)
|
461
477
|
.or(
|
462
478
|
sorted_relation
|
463
|
-
.where("#{@order_field} = ?",
|
464
|
-
.where("#{id_column} #{filter_operator} ?",
|
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
|
@@ -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.
|
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:
|
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: '
|
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: '
|
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.
|
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.
|
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.
|