rotulus 1.0.0 → 2.0.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: 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: