Class: ActiveRecord::CursorPaginator

Inherits:
Object
  • Object
show all
Defined in:
lib/active_record/cursor_paginator.rb,
lib/active_record/cursor_paginator/version.rb

Defined Under Namespace

Classes: InvalidCursorError, InvalidOrderError, ParameterError

Constant Summary collapse

DIRECTIONS =
[
  DIRECTION_FORWARD  = :forward,
  DIRECTION_BACKWARD = :backward,
].freeze
VERSION =
'0.2.0'

Instance Method Summary collapse

Constructor Details

#initialize(relation, per_page: nil, cursor: nil, direction: DIRECTION_FORWARD) ⇒ CursorPaginator

Returns a new instance of CursorPaginator.

Parameters:

  • relation (ActiveRecord::Relation)

    Relation that will be paginated.

  • per_page (Integer, nil) (defaults to: nil)

    Number of records to return.

  • cursor (String, nil) (defaults to: nil)

    Cursor to paginate

  • direction (Symbol) (defaults to: DIRECTION_FORWARD)

    :forward or :backward



25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/active_record/cursor_paginator.rb', line 25

def initialize(relation, per_page: nil, cursor: nil, direction: DIRECTION_FORWARD)
  @is_forward_pagination = direction == DIRECTION_FORWARD
  relation = relation.order(:id) if relation.order_values.empty?
  relation = relation.reverse_order unless @is_forward_pagination
  @fields = extract_order_fields(relation)
  @fields.push({ 'id' => :asc }) if @fields.last.keys.first != 'id'
  @relation = relation.reorder(@fields)
  @cursor = cursor
  @page_size = per_page
  aliases = parse_aliases
  aliases[:id] ||= "#{relation.table_name}.id"
  @aliases = aliases.with_indifferent_access
  @memos = {}
end

Instance Method Details

#end_cursorString?

Cursor of the last record on the current page

Returns:

  • (String, nil)


81
82
83
84
85
# File 'lib/active_record/cursor_paginator.rb', line 81

def end_cursor
  return if records.empty?

  cursor_for_record(records.last)
end

#extract_order_fields(relation) ⇒ Array

extract order parameter from relation as the format : [ { field1 => :asc}, { field2 => :desc}, …]

Parameters:

  • relation (ActiveRecord::Relation)

Returns:

  • (Array)


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/active_record/cursor_paginator.rb', line 43

def extract_order_fields(relation)
  orders = relation.order_values
  fields = orders.flat_map do |o|
    case o
    when Arel::Attribute # .order(arel_table[:id])
      { o.name => :asc }
    when Arel::Nodes::Ascending, # .order(id: :asc), .order(:id)
         Arel::Nodes::Descending # .order(id: :desc)
      key = o.expr.is_a?(Arel::Attributes::Attribute) ? o.expr.name : trim_quote(o.expr)
      dir = o.direction
      { key => dir }
    when String # .order('id desc')
      o.split(',').map! do |s|
        s.strip!
        matches = s.match(/\A(\w+)(?:\s+(asc|desc))?\Z/i)
        raise InvalidOrderError, 'relation has an unsupported order.' if matches.nil? || matches.length < 3

        { matches[1] => (matches[2] || 'asc').downcase.to_sym }
      end
    else # complex arel expression
      raise InvalidOrderError, 'relation has an unsupported order.'
    end
  end
  fields.flatten
end

#next_page?TrueClass, FalseClass

Check if there is another page after the current one.

Returns:

  • (TrueClass, FalseClass)


112
113
114
115
116
117
118
119
120
121
122
# File 'lib/active_record/cursor_paginator.rb', line 112

def next_page?
  if paginate_forward?
    # When paginating forward, if we managed to load one more record than
    # requested, this record will be available on the next page.
    records_plus_one.size > @page_size
  else
    # When paginating backward, in most cases,
    # we have the next page, because specified cursor may be the start cursor of a page
    true
  end
end

#paginate_forward?TrueClass, FalseClass

Check if the pagination direction is forward

Returns:

  • (TrueClass, FalseClass)


135
136
137
# File 'lib/active_record/cursor_paginator.rb', line 135

def paginate_forward?
  @is_forward_pagination
end

#previous_page?TrueClass, FalseClass

Check if there is a page before the current one.

Returns:

  • (TrueClass, FalseClass)


97
98
99
100
101
102
103
104
105
106
107
# File 'lib/active_record/cursor_paginator.rb', line 97

def previous_page?
  if paginate_forward?
    # When paginating forwards and cursor is specified,
    # we have the previous page, because specified cursor may be the end cursor of a page
    @cursor.present?
  else
    # When paginating backwards, if we managed to load one more record than
    # requested, this record will be available on the previous page.
    records_plus_one.size > @page_size
  end
end

#recordsArray<ActiveRecord>

Load the correct records and return them in the right order

Returns:



127
128
129
130
# File 'lib/active_record/cursor_paginator.rb', line 127

def records
  records = records_plus_one.first(@page_size)
  paginate_forward? ? records : records.reverse
end

#start_cursorString?

Cursor of the first record on the current page

Returns:

  • (String, nil)


72
73
74
75
76
# File 'lib/active_record/cursor_paginator.rb', line 72

def start_cursor
  return if records.empty?

  cursor_for_record(records.first)
end

#totalInteger

Get the total number of records in the given relation

Returns:

  • (Integer)


90
91
92
# File 'lib/active_record/cursor_paginator.rb', line 90

def total
  memoize(:total) { @relation.reorder('').size }
end