Class: Rotulus::Page

Inherits:
Object
  • Object
show all
Defined in:
lib/rotulus/page.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ar_relation, order: { id: :asc }, limit: nil) ⇒ Page

Creates a new Page instance representing a subset of the given ActiveRecord::Relation records sorted using the given ‘order’ definition param.

Examples:

Using expanded order definition (Recommended)

Rotulus::Page.new(User.all, order: { last_name: { direction: :asc },
                            first_name: { direction: :desc, nulls: :last },
                            ssn: { direction: :asc, distinct: true } }, limit: 3)

Using compact order definition

Rotulus::Page.new(User.all, order: { last_name: :asc, first_name: :desc, ssn: :asc }, limit: 3)

Parameters:

  • ar_relation (ActiveRecord::Relation)

    the base relation instance to be paginated

  • order (Hash<Symbol, Hash>, Hash<Symbol, Symbol>, nil) (defaults to: { id: :asc })

    the order definition of columns. Same with SQL ‘ORDER BY’, columns listed first takes precedence in the sorting of records. The order param allows 2 formats: expanded and compact. Expanded format exposes some config which allows more control in generating the optimal SQL queries to filter page records.

    Available options for each column in expanded order definition:

    • direction (Symbol) the sort direction, :asc or :desc. Default: :asc.

    • nullable (Boolean) whether a null value is expected for this column in the query result. Note that for queries with table JOINs, a column could have a null value even if the column doesn’t allow nulls in its table so :nullable might need to be set to true for such cases. Default: true if :nullable option value is nil and the column is defined as nullable in its table otherwise, false.

    • nulls (Symbol) null values sorting, :first for NULLS FIRST and :last for NULLS LAST. Applicable only if column is :nullable.

    • distinct (Boolean) whether the column value is expected to be unique in the result. Note that for queries with table JOINs, multiple rows could have the same column value even if the column has a unique index defined in its table so :distinct might need to be set to false for such cases. Default: true if :distinct option value is nil and the column is the PK of its table otherwise, false.

    • model (Class) Model where the column belongs to.

  • limit (Integer) (defaults to: nil)

    the number of records per page. Defaults to the config.page_default_limit.

Raises:

  • (InvalidLimit)

    if the :limit exceeds the configured :page_max_limit or if the :limit is not a positive number.



46
47
48
49
50
51
52
53
54
# File 'lib/rotulus/page.rb', line 46

def initialize(ar_relation, order: { id: :asc }, limit: nil)
  unless limit_valid?(limit)
    raise InvalidLimit.new("Allowed page limit is 1 up to #{config.page_max_limit}")
  end

  @ar_relation = ar_relation || model.none
  @order = Order.new(model, order)
  @limit = (limit.presence || config.page_default_limit).to_i
end

Instance Attribute Details

#ar_relationObject (readonly)

Returns the value of attribute ar_relation.



3
4
5
# File 'lib/rotulus/page.rb', line 3

def ar_relation
  @ar_relation
end

#cursorObject (readonly)

Returns the value of attribute cursor.



3
4
5
# File 'lib/rotulus/page.rb', line 3

def cursor
  @cursor
end

#limitObject (readonly)

Returns the value of attribute limit.



3
4
5
# File 'lib/rotulus/page.rb', line 3

def limit
  @limit
end

#orderObject (readonly)

Returns the value of attribute order.



3
4
5
# File 'lib/rotulus/page.rb', line 3

def order
  @order
end

Instance Method Details

#as_tableString

Returns a string showing the page’s records in table form with the ordered columns as the columns. This method is primarily used to test/debug the pagination behavior.

Returns:

  • (String)

    table



193
194
195
# File 'lib/rotulus/page.rb', line 193

def as_table
  Rotulus::PageTableizer.new(self).tableize
end

#at(token) ⇒ Page

Return a new page pointed to the given cursor(in encoded token format)

Examples:

page = Rotulus::Page.new(User.where(last_name: 'Doe'), order: { first_name: :desc }, limit: 2)
page.at('eyI6ZiI6eyJebyI6IkFjdGl2ZVN1cHBvcnQ6Okhhc2hXaXRoSW5kaWZm...')

Parameters:

  • token (String)

    Base64-encoded representation of cursor.

Returns:

  • (Page)

    page instance



65
66
67
68
69
# File 'lib/rotulus/page.rb', line 65

def at(token)
  page_copy = dup
  page_copy.at!(token)
  page_copy
end

#at!(token) ⇒ self

Point the same page instance to the given cursor(in encoded token format)

Examples:

page = Rotulus::Page.new(User.where(last_name: 'Doe'), order: { first_name: :desc }, limit: 2)
page.at!('eyI6ZiI6eyJebyI6IkFjdGl2ZVN1cHBvcnQ6Okhhc2hXaXRoSW5kaWZm...')

Parameters:

  • token (String)

    Base64-encoded representation of cursor

Returns:

  • (self)

    page instance



79
80
81
82
83
# File 'lib/rotulus/page.rb', line 79

def at!(token)
  @cursor = token.present? ? cursor_clazz.for_page_and_token!(self, token) : nil

  reload
end

#inspectObject



197
198
199
200
201
# File 'lib/rotulus/page.rb', line 197

def inspect
  cursor_info = cursor.nil? ? '' : " cursor='#{cursor}'"

  "#<#{self.class.name} ar_relation=#{ar_relation} order=#{order} limit=#{limit}#{cursor_info}>"
end

Generate a hash containing the previous and next page cursor tokens

Returns:

  • (Hash)

    the hash containing the cursor tokens



170
171
172
173
174
175
176
177
# File 'lib/rotulus/page.rb', line 170

def links
  return {} if records.empty?

  {
    previous: prev_token,
    next: next_token
  }.delete_if { |_, token| token.nil? }
end

#nextPage

Next page instance

Returns:

  • (Page)

    the next page with records after the last record of this page.



152
153
154
155
156
# File 'lib/rotulus/page.rb', line 152

def next
  return unless next?

  at next_token
end

#next?Boolean

Check if a next page exists

Returns:

  • (Boolean)

    returns true if a next page exists, otherwise returns false.



107
108
109
# File 'lib/rotulus/page.rb', line 107

def next?
  ((cursor.nil? || paged_forward?) && extra_row_returned?) || paged_back?
end

#next_tokenString

Generate the cursor token to access the next page if one exists

Returns:

  • (String)

    Base64-encoded representation of cursor



128
129
130
131
132
133
134
135
# File 'lib/rotulus/page.rb', line 128

def next_token
  return unless next?

  record = cursor_reference_record(:next)
  return if record.nil?

  cursor_clazz.new(record, :next).to_token
end

#prevPage

Previous page instance

Returns:

  • (Page)

    the previous page with records preceding the first record of this page.



161
162
163
164
165
# File 'lib/rotulus/page.rb', line 161

def prev
  return unless prev?

  at prev_token
end

#prev?Boolean

Check if a preceding page exists

Returns:

  • (Boolean)

    returns true if a previous page exists, otherwise returns false.



114
115
116
# File 'lib/rotulus/page.rb', line 114

def prev?
  (paged_back? && extra_row_returned?) || !cursor.nil? && paged_forward?
end

#prev_tokenCursor

Generate the cursor token to access the previous page if one exists

Returns:

  • (Cursor)

    Base64-encoded representation of cursor



140
141
142
143
144
145
146
147
# File 'lib/rotulus/page.rb', line 140

def prev_token
  return unless prev?

  record = cursor_reference_record(:prev)
  return if record.nil?

  cursor_clazz.new(record, :prev).to_token
end

#query_stateString

Return Hashed value of this page’s state so we can check whether the base ar_relation has changed(e.g. SQL/filters from API params). see Cursor.for_page_and_token!

Returns:

  • (String)

    the hashed state



183
184
185
186
187
# File 'lib/rotulus/page.rb', line 183

def query_state
  data = ar_relation.to_sql

  Digest::MD5.hexdigest("#{data}#{Rotulus.configuration.secret}")
end

#recordsArray<ActiveRecord::Base>

Get the records for this page. Note an extra record is fetched(limit + 1) to make it easier to check whether a next or previous page exists.

Returns:

  • (Array<ActiveRecord::Base>)

    array of records for this page.



89
90
91
92
93
# File 'lib/rotulus/page.rb', line 89

def records
  return loaded_records[1..limit] if paged_back? && extra_row_returned?

  loaded_records[0...limit]
end

#reloadself

Clear memoized records to lazily force to initiate the query again.

Returns:

  • (self)

    page instance



98
99
100
101
102
# File 'lib/rotulus/page.rb', line 98

def reload
  @loaded_records = nil

  self
end

#root?Boolean

Check if the page is the ‘root’ page; meaning, there are no preceding pages.

Returns:

  • (Boolean)

    returns true if the page is the root page, otherwise false.



121
122
123
# File 'lib/rotulus/page.rb', line 121

def root?
  cursor.nil? || !prev?
end