rotulus 1.0.0 → 2.0.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: 149f276bc80e599488039c9858f817000766d74d6a494c1447ec58167a83c424
4
- data.tar.gz: dad1c9cad5c214e88ede97573c18fbff1b65ed4a21e46b045a388910b325b5a7
3
+ metadata.gz: 07fe3f3acca52daa7170d766b516bbb39b8ad133abee1a60d635ea0c1c3521c3
4
+ data.tar.gz: b31d8781affc551740fe13878e8f269e73802c1b111bf5c29d94a4629850a9e7
5
5
  SHA512:
6
- metadata.gz: aab89cbb1f43d71ceddfe8cb1bb60d2d343f3d114109cb3d97d8650bca138d31893a1d803d7563b01ec51cca5eb3d227878f1fe2569d521edf6b9fc12ac2d364
7
- data.tar.gz: 73e0d95ca8771ca86517f6fcf3def575b5536b269f3d6cdb65c7be887f633fa872bb0d8bc479c7932a592ba0bd204f29d5ba7ba2a43acb54546981c65c62c102
6
+ metadata.gz: 0dc571a3bcde992068a7739c3ec482b44b5af9cbe269d33266177e30ae9c89219df5caea3b34cbe371880349148979fc6e322c23fbed01730e9ce1f391c532d0
7
+ data.tar.gz: 0a1fe9c87952093123df6687b9a7bb73c1f372b8d380d9d2d79be6fee2012fed0eb10f58f012242ce6a7c0e1947c76e6abfc4900fa47a021ddf3ecc579c39c7a
data/CHANGELOG.md CHANGED
@@ -15,4 +15,7 @@
15
15
 
16
16
  ## 1.0.0
17
17
  - Allow changing of ar_relation and order by default.
18
- - Make error class names consistent.
18
+ - Make error class names consistent.
19
+
20
+ ## 2.0.0
21
+ - Use multi_json instead of locking clients into using Oj gem.
data/README.md CHANGED
@@ -1,21 +1,22 @@
1
1
  # Rotulus
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/rotulus.svg)](https://badge.fury.io/rb/rotulus) [![CI](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/jsonb-uy/rotulus/branch/main/graph/badge.svg?token=OKGOWP4SH9)](https://codecov.io/gh/jsonb-uy/rotulus)
3
+ [![Gem Version](https://badge.fury.io/rb/rotulus.svg)](https://badge.fury.io/rb/rotulus) [![CI](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jsonb-uy/rotulus/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/jsonb-uy/rotulus/branch/main/graph/badge.svg?token=OKGOWP4SH9)](https://codecov.io/gh/jsonb-uy/rotulus) [![Maintainability](https://api.codeclimate.com/v1/badges/1df84f690220d9e5d260/maintainability)](https://codeclimate.com/github/jsonb-uy/rotulus/maintainability)
4
4
 
5
5
  ### Cursor-based pagination for apps built on Rails/ActiveRecord
6
6
 
7
- Cursor-based pagination is an alternative to OFFSET-based pagination that provides a more stable and predictable pagination behavior as records are being added, updated, and removed in the database through the use of an encoded cursor token.
7
+ Cursor-based pagination is an alternative to OFFSET-based pagination that provides a more stable and predictable pagination behavior as records are added, updated, and removed in the database through the use of an encoded cursor token.
8
8
 
9
9
  Some advantages of this approach are:
10
10
 
11
11
  * Reduces inaccuracies such as duplicate/skipped records due to records being actively manipulated in the DB.
12
12
  * Can significantly improve performance(with proper DB indexing on ordered columns) especially as you move forward on large datasets.
13
13
 
14
+ **TL;DR** See [ sample usage for Rails here ](#rails-usage).
14
15
 
15
16
  ## Features
16
17
 
17
- * Sort records by multiple/any number of columns
18
- * Sort records using columns from joined tables
18
+ * Paginate records sorted by any number of columns
19
+ * Paginate records sorted by columns from joined tables
19
20
  * `NULLS FIRST`/`NULLS LAST` handling
20
21
  * Allows custom cursor format
21
22
  * Built-in cursor token expiration
@@ -45,21 +46,24 @@ gem install rotulus
45
46
  ```
46
47
 
47
48
  ## Configuration
48
- Setting the environment variable `ROTULUS_SECRET` to a random string value(e.g. generate via `rails secret`) is the minimum required setup needed. But for more configuration options:
49
+ Setting the environment variable `ROTULUS_SECRET` to a random string value(e.g. generate via `rails secret`) is the minimum required setup needed.
49
50
 
51
+ <details>
52
+ <summary>More configuration options</summary>
53
+
50
54
  #### Create an initializer `config/initializers/rotulus.rb`:
51
55
 
52
- ```ruby
53
- Rotulus.configure do |config|
54
- config.page_default_limit = 5
55
- config.page_max_limit = 50
56
- config.secret = ENV["MY_ENV_VAR"]
57
- config.token_expires_in = 10800
58
- config.cursor_class = MyCursor
59
- config.restrict_order_change = false
60
- config.restrict_query_change = false
61
- end
62
- ```
56
+ ```ruby
57
+ Rotulus.configure do |config|
58
+ config.page_default_limit = 5
59
+ config.page_max_limit = 50
60
+ config.secret = ENV["MY_ENV_VAR"]
61
+ config.token_expires_in = 10800
62
+ config.cursor_class = MyCursor
63
+ config.restrict_order_change = false
64
+ config.restrict_query_change = false
65
+ end
66
+ ```
63
67
 
64
68
  | Configuration | Description |
65
69
  | ----------- | ----------- |
@@ -70,7 +74,8 @@ end
70
74
  | `restrict_order_change` | **Default: false** <br/> When `true`, raise an `OrderChanged` error when paginating with a token that was generated from a page instance with a different `:order`. <br/> When `false`, no error is raised and pagination is based on the new `:order` definition. |
71
75
  | `restrict_query_change` | **Default: false** <br/> When `true`, raise a `QueryChanged` error when paginating with a token that was generated from a page instance with a different `:ar_relation` filter/query. <br/> When `false`, no error is raised and pagination will query based on the new `:ar_relation`. |
72
76
  | `cursor_class` | **Default: Rotulus::Cursor** <br/> Cursor class responsible for encoding/decoding cursor data. Default uses Base64 encoding. see [Custom Token Format](#custom-token-format). |
73
- <br/>
77
+ <br/>
78
+ </details>
74
79
 
75
80
 
76
81
  ## Usage
@@ -128,7 +133,7 @@ In case there is no next page, `nil` is returned
128
133
  page.prev_token
129
134
  => "eyI6ZiI6eyJebyI6..."
130
135
  ```
131
- In case there is no previous page(i.e. currently in first page), `nil` is returned
136
+ In case there is no previous page(i.e., first page), `nil` is returned
132
137
 
133
138
 
134
139
  #### Navigate to the page given a cursor
@@ -172,6 +177,14 @@ page.reload
172
177
  page.reload.records
173
178
  ```
174
179
 
180
+ #### Cursor tokens hash
181
+ ```ruby
182
+ page.links
183
+
184
+ => { previous: "eyI6ZiI6efQ...", next: "eyI6ZiI6eyJ...."}
185
+ ```
186
+ If the token is `nil`, the corresponding key(previous/next) isn't included in the hash.
187
+
175
188
  #### Print page in table format for debugging
176
189
  Currently, only the columns included in `ORDER BY` are shown:
177
190
 
@@ -196,10 +209,10 @@ Instead of just specifying the column sorting such as ```{ first_name: :asc }```
196
209
  | Column Configuration | Description |
197
210
  | ----------- | ----------- |
198
211
  | `direction` | **Default: :asc**. `:asc` or `:desc` |
199
- | `nullable` | **Default: true** if column is defined as nullable in its table, _false_ otherwise. <br/><br />Whether a null value is expected for this column in the result set. <br /><br/>**Note:** <br/>- Not setting this to _true_ when there are possible rows with NULL values for the specific column in the DB won't return those records. <br/> - In queries with table (outer)`JOIN`s, a column in the result could have a NULL value even if the column doesn't allow nulls in its table. So set `nullable` to _true_ for such cases.
212
+ | `nullable` | **Default: true** if the column is defined as nullable in its table, _false_ otherwise. <br/><br />Whether a null value is expected for this column in the result set. <br /><br/>**Note:** <br/>- Not setting this to _true_ when there are possible rows with NULL values for the specific column in the DB won't return those records. <br/> - In queries with table (outer)`JOIN`s, a column in the result could have a NULL value even if the column doesn't allow nulls in its table. So set `nullable` to _true_ for such cases.
200
213
  | `nulls` | **Default:**<br/>- MySQL and SQLite: `:first` if `direction` is `:asc`, otherwise `:last`<br/>- PostgreSQL: `:last` if `direction` is `:asc`, otherwise `:first`<br/><br/>Tells whether rows with NULL column values comes before/after the records with non-null values. Applicable only if column is `nullable`. |
201
214
  | `distinct` | **Default: true** if the column is the primary key of its table, _false_ otherwise.<br/><br /> Tells whether rows in the result are expected to have unique values for this column. <br/><br />**Note:**<br/>- In queries with table `JOIN`s, multiple rows could have the same column value even if the column has a unique index in its table. So set `distinct` to false for such cases. |
202
- | `model` | **Default:**<br/> - the model of the base AR relation passed to `Rotulus::Page.new(<ar_relation>)` if column name has no prefix(e.g. `first_name`) and the AR relation model has a column matching the column name.<br/>- the model of the base AR relation passed to `Rotulus::Page.new(<ar_relation>)` if column name has a prefix(e.g. `users.first_name`) and thre prefix matches the AR relation's table name and the table has a column matching the column name. <br/><br/>Model where this column belongs. This allows the gem to infer the nullability and uniqueness from the column definition in its table instead of manually setting the `nullable` or `distinct` options and to also automatically prefix the column name with the table name. <br/>|
215
+ | `model` | **Default:**<br/> - the model of the base AR relation passed to `Rotulus::Page.new(<ar_relation>)` if column name has no prefix(e.g. `first_name`) and the AR relation model has a column matching the column name.<br/>- the model of the base AR relation passed to `Rotulus::Page.new(<ar_relation>)` if column name has a prefix(e.g. `users.first_name`) and the prefix matches the AR relation's table name and the table has a column matching the column name. <br/><br/>Model where this column belongs. This allows the gem to infer the nullability and uniqueness from the column definition in its table instead of manually setting the `nullable` or `distinct` options and to also automatically prefix the column name with the table name. <br/>|
203
216
 
204
217
 
205
218
  ##### Example:
@@ -262,7 +275,7 @@ page = Rotulus::Page.new(items, order: order_by, limit: 2)
262
275
  ```
263
276
 
264
277
  Some notes for the example above: <br/>
265
- 1. `oi.id` is needed to uniquely identify and serve as the tie-breaker for `Item`s that have `OrderItem`s having the same item_count and name. The combination of `oi.item_count`, `items.name`, and `oi.id` makes those record unique in the dataset. <br/>
278
+ 1. `oi.id` is needed to uniquely identify and serve as the tie-breaker for `Item`s that have `OrderItem`s having the same item_count and name. The combination of `oi.item_count`, `items.name`, and `oi.id` makes those records unique in the dataset. <br/>
266
279
  2. `id` is translated to `items.id` and is needed to uniquely identify and serve as the tie-breaker for `Item`s that have NO `OrderItem`s. The combination of `oi.item_count`(NULL), `items.name`, `oi.id`(NULL), and `items.id` makes those record unique in the dataset. Although, this can be removed in the configuration above as the `Item` table's primary key will be automatically added as the last `ORDER BY` column if it isn't included yet.<br/>
267
280
  3. Explicitly setting the `model: OrderItem` in joined table columns is required for now.
268
281
 
@@ -291,7 +304,51 @@ page = Rotulus::Page.new(items, order: order_by, limit: 2)
291
304
 
292
305
  ```
293
306
 
294
- <br/>
307
+ ### Rails Usage
308
+
309
+ ##### Controller example 1:
310
+
311
+ ```ruby
312
+ def index
313
+ page = Rotulus::Page.new(User.all, order: index_order, limit: params.dig(:page, :limit))
314
+ .at!(params[:cursor])
315
+ render json: { data: page.records }.merge!(page.links) # `page.links` contain the `cursor` value for next/prev pages.
316
+ end
317
+
318
+ private
319
+
320
+ def index_order
321
+ { first_name: :asc,
322
+ last_name: { direction: :desc, nulls: :last },
323
+ email: { direction: :asc, distinct: true } }
324
+ end
325
+ ```
326
+
327
+ APIs usually allow clients to specify which columns to sort through a parameter. You may use the [sort_param](https://rubygems.org/gems/sort_param) gem to support this:
328
+
329
+ ##### Controller example 2:
330
+
331
+ ```ruby
332
+ def index
333
+ page = Rotulus::Page.new(User.all, order: index_order, limit: params.dig(:page, :limit))
334
+ .at!(params[:cursor])
335
+ render json: { data: page.records }.merge!(page.links)
336
+ end
337
+
338
+ private
339
+
340
+ # Allow clients to sort by first_name, last_name, and/or email.
341
+ # example sort values:
342
+ # a. params[:sort] = +users.last_name,-users.email
343
+ # b. params[:sort] = -first_name
344
+ def index_order
345
+ SortParam.define do
346
+ field 'users.first_name'
347
+ field 'users.last_name', nulls: :last, nullable: true
348
+ field 'users.email', distinct: true
349
+ end.load!(params[:sort].presence || '+users.first_name')
350
+ end
351
+ ```
295
352
 
296
353
  ### Errors
297
354
 
@@ -315,7 +372,7 @@ Cursor-based pagination uses a reference point/record to fetch the previous or n
315
372
  * Columns used in `ORDER BY` would need to be indexed as they will be used in filtering.
316
373
 
317
374
 
318
- #### Sample SQL generated snippets
375
+ #### Sample SQL-generated snippets
319
376
 
320
377
  ##### Example 1: With order by `id` only
321
378
  ###### Ruby
@@ -376,20 +433,22 @@ To navigate between pages, a cursor is used. The cursor token is a Base64 encode
376
433
 
377
434
  ```json
378
435
  {
379
- "f": {"users.first_name": "Jane", "users.id": 2},
436
+ "f": { "users.first_name": "Jane", "users.id": 2 },
380
437
  "d": "next",
381
- "s": "251177d65873aa37057dba548ecba82f",
382
- "c": 1672502400
438
+ "c": 1672502400,
439
+ "cs": "fe6ac1a1d6a1fc1b7f842b388639f63b",
440
+ "os": "62186497a8073f9c7072389b73c6c60c",
441
+ "qs": "7a5053198709df924dd5ec1752ee4e6b"
383
442
  }
384
443
  ```
385
444
  1. `f` - contains the record values from the last record of the current page. Only the columns included in the `ORDER BY` are included. Note also that the unique column `users.id` is included as a tie-breaker.
386
445
  2. `d` - the pagination direction. `next` or `prev` set of records from the reference values in "f".
387
- 3. `cs` - the cursor state needed for integrity checking, restrict clients/third-parties from generating their own (unsafe)tokens, or from tampering the data of an existing token.
446
+ 3. `cs` - the cursor state needed for integrity checking, restricting clients/third-parties from generating their own (unsafe)tokens, or from tampering with the data of an existing token.
388
447
  4. `os` - the order state needed to detect whether the order definition changed.
389
- 5. `qs` - the base AR relation state neede to detect whether the ar_relation has changed (e.g. filter/query changed due to API params).
448
+ 5. `qs` - the base AR relation state needed to detect whether the ar_relation has changed (e.g. filter/query changed due to API params).
390
449
  4. `c` - cursor token issuance time.
391
450
 
392
- A condition generated from the cursor above would look like:
451
+ A condition generated from the cursor above would look like this:
393
452
 
394
453
  ```sql
395
454
  WHERE users.first_name >= 'Jane' AND (
@@ -26,14 +26,11 @@ module Rotulus
26
26
  def initialize(model, name, direction: :asc, nullable: nil, nulls: nil, distinct: nil)
27
27
  @model = model
28
28
  @name = name.to_s
29
- unless name_valid?
30
- raise Rotulus::InvalidColumn.new("Column/table name must contain letters, digits (0-9), or \
31
- underscores and must begin with a letter or underscore.".squish)
32
- end
29
+ validate_name!
33
30
 
34
- @direction = direction.to_s.downcase == 'desc' ? :desc : :asc
35
- @distinct = (distinct.nil? ? primary_key? : distinct).presence || false
36
- @nullable = (nullable.nil? ? metadata&.null : nullable).presence || false
31
+ @direction = sort_direction(direction)
32
+ @distinct = uniqueness(distinct)
33
+ @nullable = nullability(nullable)
37
34
  @nulls = nulls_order(nulls)
38
35
  end
39
36
 
@@ -150,8 +147,27 @@ module Rotulus
150
147
  Rotulus.db.default_nulls_order(direction)
151
148
  end
152
149
 
150
+ def nullability(nullable)
151
+ (nullable.nil? ? metadata&.null : nullable).presence || false
152
+ end
153
+
153
154
  def primary_key?
154
155
  unprefixed_name == model.primary_key
155
156
  end
157
+
158
+ def sort_direction(direction)
159
+ direction.to_s.downcase == 'desc' ? :desc : :asc
160
+ end
161
+
162
+ def uniqueness(unique)
163
+ (unique.nil? ? primary_key? : unique).presence || false
164
+ end
165
+
166
+ def validate_name!
167
+ return if name_valid?
168
+
169
+ raise Rotulus::InvalidColumn.new("Column/table name must contain letters, digits (0-9), or \
170
+ underscores and must begin with a letter or underscore.".squish)
171
+ end
156
172
  end
157
173
  end
@@ -38,18 +38,19 @@ module Rotulus
38
38
  end
39
39
 
40
40
  def nullable_filter_condition
41
- if seek_to_null_direction?
42
- return tie_break null_condition if value.nil?
41
+ return seek_to_null_direction_condition if seek_to_null_direction?
42
+ return filter_condition unless value.nil?
43
43
 
44
- condition = "#{seek_condition} OR #{null_condition}"
45
- return condition if column.distinct?
44
+ "#{not_null_condition} OR (#{tie_break(null_condition)})"
45
+ end
46
46
 
47
- prefilter("(#{condition}) OR (#{tie_break(identity)})")
48
- else
49
- return filter_condition unless value.nil?
47
+ def seek_to_null_direction_condition
48
+ return tie_break null_condition if value.nil?
50
49
 
51
- "#{not_null_condition} OR (#{tie_break(null_condition)})"
52
- end
50
+ condition = "#{seek_condition} OR #{null_condition}"
51
+ return condition if column.distinct?
52
+
53
+ prefilter("(#{condition}) OR (#{tie_break(identity)})")
53
54
  end
54
55
 
55
56
  def identity
@@ -14,12 +14,12 @@ module Rotulus
14
14
  # @raise [QueryChanged] if token generated from a page with a different `:ar_relation`.
15
15
  def for_page_and_token!(page, token)
16
16
  data = decode(token)
17
- reference_record = Record.new(page, data[:f])
18
- direction = data[:d]
19
- created_at = Time.at(data[:c]).utc
20
- cursor_state = data[:cs].presence
21
- order_state = data[:os].presence
22
- query_state = data[:qs].presence
17
+ reference_record = Record.new(page, data['f'])
18
+ direction = data['d']
19
+ created_at = Time.at(data['c']).utc
20
+ cursor_state = data['cs'].presence
21
+ order_state = data['os'].presence
22
+ query_state = data['qs'].presence
23
23
 
24
24
  cursor = new(reference_record, direction, created_at: created_at)
25
25
 
@@ -48,8 +48,8 @@ module Rotulus
48
48
  # of the previous page if page direction is `:next` or the first record of the next
49
49
  # page if page direction is `:prev`.
50
50
  def decode(token)
51
- Oj.load(Base64.urlsafe_decode64(token))
52
- rescue ArgumentError, Oj::ParseError => e
51
+ MultiJson.load(Base64.urlsafe_decode64(token))
52
+ rescue ArgumentError, MultiJson::ParseError => e
53
53
  raise InvalidCursor.new("Invalid Cursor: #{e.message}")
54
54
  end
55
55
 
@@ -58,7 +58,7 @@ module Rotulus
58
58
  # @param token_data [Hash] Cursor token data hash
59
59
  # @return token [String] String token for this cursor that can be used as param to Page#at.
60
60
  def encode(token_data)
61
- Base64.urlsafe_encode64(Oj.dump(token_data, symbol_keys: true))
61
+ Base64.urlsafe_encode64(MultiJson.dump(token_data))
62
62
  end
63
63
  end
64
64
 
@@ -104,12 +104,12 @@ module Rotulus
104
104
  #
105
105
  # @return [String] the token encoded in Base64.
106
106
  def to_token
107
- @token ||= self.class.encode(f: record.values,
108
- d: direction,
109
- c: created_at.to_i,
110
- cs: state,
111
- os: page.order_state,
112
- qs: page.query_state)
107
+ @token ||= self.class.encode('f' => record.values.as_json,
108
+ 'd' => direction,
109
+ 'c' => created_at.to_i,
110
+ 'cs' => state,
111
+ 'os' => page.order_state,
112
+ 'qs' => page.query_state)
113
113
  end
114
114
  alias to_s to_token
115
115
 
data/lib/rotulus/order.rb CHANGED
@@ -10,7 +10,7 @@ module Rotulus
10
10
  @raw_hash = raw_hash&.with_indifferent_access || {}
11
11
  @definition = {}
12
12
 
13
- build_column_definitions
13
+ build_column_definitions!
14
14
 
15
15
  return if has_tiebreaker?
16
16
 
@@ -80,7 +80,7 @@ module Rotulus
80
80
  #
81
81
  # @return [String] the hashed state
82
82
  def state
83
- data = Oj.dump(to_h, mode: :rails)
83
+ data = MultiJson.dump(to_h)
84
84
 
85
85
  Digest::MD5.hexdigest("#{data}#{Rotulus.configuration.secret}")
86
86
  end
@@ -153,39 +153,42 @@ module Rotulus
153
153
  !definition["#{ar_table}.#{ar_model_primary_key}"].nil?
154
154
  end
155
155
 
156
- def build_column_definitions
156
+ def build_column_definitions!
157
157
  raw_hash.each do |column_name, options|
158
158
  column_name = column_name.to_s
159
159
 
160
- unless options.is_a?(Hash)
161
- options = if options.to_s.downcase == 'desc'
162
- { direction: :desc }
163
- else
164
- { direction: :asc }
165
- end
166
- end
167
-
168
- model = column_model(options[:model].presence, column_name)
160
+ options = normalize_column_options(options)
161
+ model = column_model(options.delete(:model), column_name)
169
162
  column = Column.new(model,
170
163
  column_name,
171
164
  direction: options[:direction],
172
165
  nulls: options[:nulls],
173
166
  nullable: options[:nullable],
174
167
  distinct: options[:distinct])
175
- next unless definition[column.prefixed_name].nil?
176
-
177
- definition[column.prefixed_name] = column
168
+ definition[column.prefixed_name] ||= column
178
169
  end
179
170
 
180
171
  # Add tie-breaker using the PK
181
- unless primary_key_ordered?
182
- pk_column = Column.new(ar_model, ar_model_primary_key, direction: :asc)
183
- definition[pk_column.prefixed_name] = pk_column
184
- end
172
+ add_pk_tiebreaker_column!
185
173
 
186
174
  columns.first.as_leftmost!
187
175
  end
188
176
 
177
+ def add_pk_tiebreaker_column!
178
+ return if primary_key_ordered?
179
+
180
+ pk_column = Column.new(ar_model, ar_model_primary_key, direction: :asc)
181
+
182
+ definition[pk_column.prefixed_name] = pk_column
183
+ end
184
+
185
+ def normalize_column_options(options)
186
+ return options if options.is_a?(Hash)
187
+ return { direction: :desc } if options.to_s.downcase == 'desc'
188
+
189
+ { direction: :asc }
190
+ end
191
+
189
192
  # Returns an array of SELECT statement alias of the ordered columns
190
193
  #
191
194
  # @return [Array<String>] column SELECT aliases
data/lib/rotulus/page.rb CHANGED
@@ -77,7 +77,7 @@ module Rotulus
77
77
  # @param token [String] Base64-encoded representation of cursor
78
78
  # @return [self] page instance
79
79
  def at!(token)
80
- @cursor = token.present? ? cursor_clazz.for_page_and_token!(self, token) : nil
80
+ @cursor = token.present? ? config.cursor_class.for_page_and_token!(self, token) : nil
81
81
 
82
82
  reload
83
83
  end
@@ -131,7 +131,7 @@ module Rotulus
131
131
  record = cursor_reference_record(:next)
132
132
  return if record.nil?
133
133
 
134
- cursor_clazz.new(record, :next).to_token
134
+ config.cursor_class.new(record, :next).to_token
135
135
  end
136
136
 
137
137
  # Generate the cursor token to access the previous page if one exists
@@ -143,7 +143,7 @@ module Rotulus
143
143
  record = cursor_reference_record(:prev)
144
144
  return if record.nil?
145
145
 
146
- cursor_clazz.new(record, :prev).to_token
146
+ config.cursor_class.new(record, :prev).to_token
147
147
  end
148
148
 
149
149
  # Next page instance
@@ -202,6 +202,8 @@ module Rotulus
202
202
 
203
203
  private
204
204
 
205
+ delegate :model, to: :ar_relation, prefix: false
206
+
205
207
  # If this is the root page or when paginating forward(#paged_forward), limit+1
206
208
  # includes the first record of the next page. This lets us know whether there is a page
207
209
  # succeeding the current page. When paginating backwards(#paged_back?), the limit+1 includes the
@@ -243,15 +245,11 @@ module Rotulus
243
245
  # to filter next/prev page's records. Alias and normalize those columns so we can access
244
246
  # the values using record#slice.
245
247
  def select_columns
246
- base_select_values = ar_relation.select_values.presence || [select_all_sql]
248
+ base_select_values = ar_relation.select_values.presence || [Rotulus.db.select_all_sql(model.table_name)]
247
249
  base_select_values << order.select_sql
248
250
  base_select_values
249
251
  end
250
252
 
251
- def select_all_sql
252
- Rotulus.db.select_all_sql(model.table_name)
253
- end
254
-
255
253
  def order_by_sql
256
254
  return order.reversed_sql if paged_back?
257
255
 
@@ -265,16 +263,8 @@ module Rotulus
265
263
  limit >= 1 && limit <= config.page_max_limit
266
264
  end
267
265
 
268
- def model
269
- ar_relation.model
270
- end
271
-
272
266
  def config
273
267
  @config ||= Rotulus.configuration
274
268
  end
275
-
276
- def cursor_clazz
277
- @cursor_clazz ||= config.cursor_class
278
- end
279
269
  end
280
270
  end
@@ -1,3 +1,3 @@
1
1
  module Rotulus
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '2.0.0'.freeze
3
3
  end
data/lib/rotulus.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'active_record'
2
2
  require 'active_support'
3
3
  require 'active_support/core_ext/string/inquiry'
4
- require 'oj'
4
+ require 'multi_json'
5
5
  require 'rotulus/version'
6
6
  require 'rotulus/configuration'
7
7
  require 'rotulus/db/database'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rotulus
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uy Jayson B
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-25 00:00:00.000000000 Z
11
+ date: 2023-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -51,19 +51,19 @@ dependencies:
51
51
  - !ruby/object:Gem::Version
52
52
  version: '7.1'
53
53
  - !ruby/object:Gem::Dependency
54
- name: oj
54
+ name: multi_json
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
- - - ">="
57
+ - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '0'
59
+ version: '1.15'
60
60
  type: :runtime
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - ">="
64
+ - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: '0'
66
+ version: '1.15'
67
67
  description: Cursor-based pagination for Rails/ActiveRecord apps with multiple column
68
68
  sort and custom cursor format support for a more stable and predictable pagination.
69
69
  email: