Class: Rotulus::Cursor

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(record, direction, created_at: nil) ⇒ Cursor

Returns a new instance of Cursor.

Parameters:

  • record (Record)

    the last(first if direction is ‘:prev`) record of page containing the ordered column’s values that will be used to generate the next/prev page query.

  • direction (Symbol)

    the cursor direction, ‘:next` for next page or `:prev` for previous page

  • created_at (Time) (defaults to: nil)

    only needed when deserializing a Cursor from a token. The time when the cursor was last initialized. see Cursor.from_page_and_token!



75
76
77
78
79
80
81
# File 'lib/rotulus/cursor.rb', line 75

def initialize(record, direction, created_at: nil)
  @record = record
  @direction = direction.to_sym
  @created_at = created_at.presence || Time.current

  validate!
end

Instance Attribute Details

#created_atObject (readonly)

Returns the value of attribute created_at.



65
66
67
# File 'lib/rotulus/cursor.rb', line 65

def created_at
  @created_at
end

#directionObject (readonly)

Returns the value of attribute direction.



65
66
67
# File 'lib/rotulus/cursor.rb', line 65

def direction
  @direction
end

#recordObject (readonly)

Returns the value of attribute record.



65
66
67
# File 'lib/rotulus/cursor.rb', line 65

def record
  @record
end

Class Method Details

.decode(token) ⇒ Object

Decode the given encoded cursor token

@param token [String] Encoded cursor token
@return [Hash] Cursor data hash containing the cursor direction(:next, :prev),
  cursor's state, and the ordered column values of the reference record: last record
  of the previous page if page direction is `:next` or the first record of the next
  page if page direction is `:prev`.


50
51
52
53
54
# File 'lib/rotulus/cursor.rb', line 50

def decode(token)
  Oj.load(Base64.urlsafe_decode64(token))
rescue ArgumentError, Oj::ParseError => e
  raise InvalidCursor.new("Invalid Cursor: #{e.message}")
end

.encode(token_data) ⇒ Object

Encode cursor data hash

@param token_data [Hash] Cursor token data hash
@return token [String] String token for this cursor that can be used as param to Page#at.


60
61
62
# File 'lib/rotulus/cursor.rb', line 60

def encode(token_data)
  Base64.urlsafe_encode64(Oj.dump(token_data, symbol_keys: true))
end

.for_page_and_token!(page, token) ⇒ Object

Initialize a Cursor instance for the given page instance and encoded token.

@param page [Page] Page instance
@param token [String] Base64-encoded string data
@return [Cursor] Cursor

@raise [InvalidCursor] if the token can't be decoded or if the cursor data was tampered.
@raise [OrderChanged] if token generated from a page with a different `:order` definition.
@raise [QueryChanged] if token generated from a page with a different `:ar_relation`.

Raises:



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/rotulus/cursor.rb', line 15

def for_page_and_token!(page, token)
  data = decode(token)
  reference_record = Record.new(page, data[:f])
  direction = data[:d]
  created_at = Time.at(data[:c]).utc
  cursor_state = data[:cs].presence
  order_state = data[:os].presence
  query_state = data[:qs].presence

  cursor = new(reference_record, direction, created_at: created_at)

  raise InvalidCursor if cursor.state != cursor_state

  if page.order_state != order_state
    raise OrderChanged if Rotulus.configuration.restrict_order_change?

    return nil
  end

  if page.query_state != query_state
    raise QueryChanged if Rotulus.configuration.restrict_query_change?

    return nil
  end

  cursor
end

Instance Method Details

#expired?Boolean

Checks if the cursor is expired

Returns:

  • (Boolean)

    returns true if cursor is expired



129
130
131
132
133
# File 'lib/rotulus/cursor.rb', line 129

def expired?
  return false if config.token_expires_in.nil? || created_at.nil?

  (created_at + config.token_expires_in) < Time.current
end

#next?Boolean

Returns true if the cursor should retrieve the ‘next’ records from the last record of the previous page. Otherwise, returns false.

Returns:

  • (Boolean)

    returns true if the cursor should retrieve the ‘next’ records from the last record of the previous page. Otherwise, returns false.



85
86
87
# File 'lib/rotulus/cursor.rb', line 85

def next?
  direction == :next
end

#prev?Boolean

Returns true if the cursor should retrieve the ‘previous’ records from the first record of a page. Otherwise, returns false.

Returns:

  • (Boolean)

    returns true if the cursor should retrieve the ‘previous’ records from the first record of a page. Otherwise, returns false.



91
92
93
# File 'lib/rotulus/cursor.rb', line 91

def prev?
  !next?
end

#sqlString

Generate the SQL condition to filter the records of the next/previous page. The condition is generated based on the order definition and the referenced record’s values.

Returns:

  • (String)

    the SQL ‘where’ condition to get the next or previous page’s records.



99
100
101
# File 'lib/rotulus/cursor.rb', line 99

def sql
  @sql ||= Arel.sql(record.sql_seek_condition(direction))
end

#stateString

Generate a ‘state’ string for integrity checking of the reference record, direction, and created_at data from a decoded Cursor token.

Returns:

  • (String)

    the hashed state



120
121
122
123
124
# File 'lib/rotulus/cursor.rb', line 120

def state
  state_data = "#{record.state}#{direction}#{created_at.to_i}#{secret}"

  Digest::MD5.hexdigest(state_data)
end

#to_tokenString Also known as: to_s

Generate the token: a Base64-encoded string representation of this cursor

Returns:

  • (String)

    the token encoded in Base64.



106
107
108
109
110
111
112
113
# File 'lib/rotulus/cursor.rb', line 106

def to_token
  @token ||= self.class.encode(f: record.values.as_json,
                               d: direction,
                               c: created_at.to_i,
                               cs: state,
                               os: page.order_state,
                               qs: page.query_state)
end