Class: Apes::PaginationCursor

Inherits:
Object
  • Object
show all
Defined in:
lib/apes/pagination_cursor.rb

Overview

A cursor that can be sent to the client, received unmodified and retrieved later to paginate results.

Constant Summary

DEFAULT_SIZE =

The default size of a pagination page.

25
TIMESTAMP_FORMAT =

Format to serialize timestamp when using them for pagination.

"%FT%T.%6N%z".freeze

Instance Attribute Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Apes::PaginationCursor) initialize(params = {}, field = :page, count_field = :count)

Creates a new cursor.

Parameters:

  • params (Hash) (defaults to: {})

    The request parameters.

  • field (Symbol) (defaults to: :page)

    The parameters field where to lookup for the serialized cursor.

  • count_field (Symbol) (defaults to: :count)

    The parameters field where to lookup for the overriding cursor size.



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/apes/pagination_cursor.rb', line 32

def initialize(params = {}, field = :page, count_field = :count)
  begin
    payload = JWT.decode(params[field], jwt_secret, true, {algorithm: "HS256", verify_aud: true, aud: "pagination"}).dig(0, "sub")

    extract_payload(payload)
  rescue
    default_payload
  end

  # Sanitization
  sanitize(count_field, params)
end

Instance Attribute Details

- (IO|String) direction

Returns Which page to get in this iteration.

Returns:

  • (IO|String)

    Which page to get in this iteration.



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
42
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/apes/pagination_cursor.rb', line 17

class PaginationCursor
  # The default size of a pagination page.
  DEFAULT_SIZE = 25

  # Format to serialize timestamp when using them for pagination.
  TIMESTAMP_FORMAT = "%FT%T.%6N%z".freeze

  attr_accessor :value, :use_offset, :direction, :size

  # Creates a new cursor.
  #
  # @param params [Hash] The request parameters.
  # @param field [Symbol] The parameters field where to lookup for the serialized cursor.
  # @param count_field [Symbol] The parameters field where to lookup for the overriding cursor size.
  # @return [Apes::PaginationCursor] A new cursor instance.
  def initialize(params = {}, field = :page, count_field = :count)
    begin
      payload = JWT.decode(params[field], jwt_secret, true, {algorithm: "HS256", verify_aud: true, aud: "pagination"}).dig(0, "sub")

      extract_payload(payload)
    rescue
      default_payload
    end

    # Sanitization
    sanitize(count_field, params)
  end

  # Get the operator (`>` or `<`) for the query according to the direction and the provided ordering.
  #
  # @param order [Symbol] The order to use.
  # @return [String] The operator to use for the query.
  def operator(order)
    if direction == "next"
      order == :asc ? ">" : "<" # Descending order means newer results first
    else
      order == :asc ? "<" : ">" # Descending order means newer results first
    end
  end

  # Verifies whether a specific page might exist for the given collection.
  #
  # @param page [String] The page to check. It can be `first`, `next`, `prev` or `previous`.
  # @param collection [Enumerable] The collection to analyze.
  # @return [Boolean] Returns `true` if the page might exist for the collection, `false` otherwise.
  def might_exist?(page, collection)
    case page.to_s
    when "first" then true
    when "next" then collection.present?
    else value.present? && collection.present? # Previous
    end
  end

  # Serializes the cursor to send it to the client.
  #
  # @param collection [Enumerable] The collection to analyze.
  # @param page [String] The page to return. It can be `first`, `next`, `prev` or `previous`.
  # @param field [Symbol] When not using offset based pagination, the field to consider for generation.
  # @param size [Fixnum] The number of results to advance when using offset based pagination.
  # @param use_offset [Boolean] Whether to use offset based pagination.
  # @return [String] The serialized cursor.
  def save(collection, page, field: :id, size: nil, use_offset: nil)
    size ||= self.size
    use_offset = self.use_offset if use_offset.nil?
    direction, value = use_offset ? update_with_offset(page, size) : update_with_field(page, collection, field)

    value = value.strftime(TIMESTAMP_FORMAT) if value.respond_to?(:strftime)

    JWT.encode({aud: "pagination", sub: {value: value, use_offset: use_offset, direction: direction, size: size}}, jwt_secret, "HS256")
  end
  alias_method :serialize, :save

  private

  # :nodoc:
  def default_payload
    @value = nil
    @direction = "next"
    @size = 0
    @use_offset = false
  end

  # :nodoc:
  def extract_payload(payload)
    @value = payload["value"]
    @direction = payload["direction"]
    @size = payload["size"]
    @use_offset = payload["use_offset"]
  end

  # :nodoc:
  def sanitize(count_field, params)
    @direction = "next" unless ["prev", "previous"].include?(@direction)
    @size = params[count_field].to_integer if params[count_field].present?
    @size = DEFAULT_SIZE if @size < 1
  end

  # :nodoc:
  def update_with_field(type, collection, field)
    case type.ensure_string
    when "next"
      direction = "next"
      value = collection.last&.send(field)
    when "prev", "previous"
      direction = "previous"
      value = collection.first&.send(field)
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  # :nodoc:
  def update_with_offset(type, size)
    case type.ensure_string
    when "next"
      direction = "next"
      value = self.value + size
    when "prev", "previous"
      direction = "previous"
      value = [0, self.value - size].max
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  def jwt_secret
    Apes::RuntimeConfiguration.jwt_token
  end
end

- (IO|String) size

Returns The size of the pagination page.

Returns:

  • (IO|String)

    The size of the pagination page.



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
42
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/apes/pagination_cursor.rb', line 17

class PaginationCursor
  # The default size of a pagination page.
  DEFAULT_SIZE = 25

  # Format to serialize timestamp when using them for pagination.
  TIMESTAMP_FORMAT = "%FT%T.%6N%z".freeze

  attr_accessor :value, :use_offset, :direction, :size

  # Creates a new cursor.
  #
  # @param params [Hash] The request parameters.
  # @param field [Symbol] The parameters field where to lookup for the serialized cursor.
  # @param count_field [Symbol] The parameters field where to lookup for the overriding cursor size.
  # @return [Apes::PaginationCursor] A new cursor instance.
  def initialize(params = {}, field = :page, count_field = :count)
    begin
      payload = JWT.decode(params[field], jwt_secret, true, {algorithm: "HS256", verify_aud: true, aud: "pagination"}).dig(0, "sub")

      extract_payload(payload)
    rescue
      default_payload
    end

    # Sanitization
    sanitize(count_field, params)
  end

  # Get the operator (`>` or `<`) for the query according to the direction and the provided ordering.
  #
  # @param order [Symbol] The order to use.
  # @return [String] The operator to use for the query.
  def operator(order)
    if direction == "next"
      order == :asc ? ">" : "<" # Descending order means newer results first
    else
      order == :asc ? "<" : ">" # Descending order means newer results first
    end
  end

  # Verifies whether a specific page might exist for the given collection.
  #
  # @param page [String] The page to check. It can be `first`, `next`, `prev` or `previous`.
  # @param collection [Enumerable] The collection to analyze.
  # @return [Boolean] Returns `true` if the page might exist for the collection, `false` otherwise.
  def might_exist?(page, collection)
    case page.to_s
    when "first" then true
    when "next" then collection.present?
    else value.present? && collection.present? # Previous
    end
  end

  # Serializes the cursor to send it to the client.
  #
  # @param collection [Enumerable] The collection to analyze.
  # @param page [String] The page to return. It can be `first`, `next`, `prev` or `previous`.
  # @param field [Symbol] When not using offset based pagination, the field to consider for generation.
  # @param size [Fixnum] The number of results to advance when using offset based pagination.
  # @param use_offset [Boolean] Whether to use offset based pagination.
  # @return [String] The serialized cursor.
  def save(collection, page, field: :id, size: nil, use_offset: nil)
    size ||= self.size
    use_offset = self.use_offset if use_offset.nil?
    direction, value = use_offset ? update_with_offset(page, size) : update_with_field(page, collection, field)

    value = value.strftime(TIMESTAMP_FORMAT) if value.respond_to?(:strftime)

    JWT.encode({aud: "pagination", sub: {value: value, use_offset: use_offset, direction: direction, size: size}}, jwt_secret, "HS256")
  end
  alias_method :serialize, :save

  private

  # :nodoc:
  def default_payload
    @value = nil
    @direction = "next"
    @size = 0
    @use_offset = false
  end

  # :nodoc:
  def extract_payload(payload)
    @value = payload["value"]
    @direction = payload["direction"]
    @size = payload["size"]
    @use_offset = payload["use_offset"]
  end

  # :nodoc:
  def sanitize(count_field, params)
    @direction = "next" unless ["prev", "previous"].include?(@direction)
    @size = params[count_field].to_integer if params[count_field].present?
    @size = DEFAULT_SIZE if @size < 1
  end

  # :nodoc:
  def update_with_field(type, collection, field)
    case type.ensure_string
    when "next"
      direction = "next"
      value = collection.last&.send(field)
    when "prev", "previous"
      direction = "previous"
      value = collection.first&.send(field)
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  # :nodoc:
  def update_with_offset(type, size)
    case type.ensure_string
    when "next"
      direction = "next"
      value = self.value + size
    when "prev", "previous"
      direction = "previous"
      value = [0, self.value - size].max
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  def jwt_secret
    Apes::RuntimeConfiguration.jwt_token
  end
end

- (Boolean) use_offset

Returns Whether to use offset based pagination rather than collection fields values.

Returns:

  • (Boolean)

    Whether to use offset based pagination rather than collection fields values.



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
42
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/apes/pagination_cursor.rb', line 17

class PaginationCursor
  # The default size of a pagination page.
  DEFAULT_SIZE = 25

  # Format to serialize timestamp when using them for pagination.
  TIMESTAMP_FORMAT = "%FT%T.%6N%z".freeze

  attr_accessor :value, :use_offset, :direction, :size

  # Creates a new cursor.
  #
  # @param params [Hash] The request parameters.
  # @param field [Symbol] The parameters field where to lookup for the serialized cursor.
  # @param count_field [Symbol] The parameters field where to lookup for the overriding cursor size.
  # @return [Apes::PaginationCursor] A new cursor instance.
  def initialize(params = {}, field = :page, count_field = :count)
    begin
      payload = JWT.decode(params[field], jwt_secret, true, {algorithm: "HS256", verify_aud: true, aud: "pagination"}).dig(0, "sub")

      extract_payload(payload)
    rescue
      default_payload
    end

    # Sanitization
    sanitize(count_field, params)
  end

  # Get the operator (`>` or `<`) for the query according to the direction and the provided ordering.
  #
  # @param order [Symbol] The order to use.
  # @return [String] The operator to use for the query.
  def operator(order)
    if direction == "next"
      order == :asc ? ">" : "<" # Descending order means newer results first
    else
      order == :asc ? "<" : ">" # Descending order means newer results first
    end
  end

  # Verifies whether a specific page might exist for the given collection.
  #
  # @param page [String] The page to check. It can be `first`, `next`, `prev` or `previous`.
  # @param collection [Enumerable] The collection to analyze.
  # @return [Boolean] Returns `true` if the page might exist for the collection, `false` otherwise.
  def might_exist?(page, collection)
    case page.to_s
    when "first" then true
    when "next" then collection.present?
    else value.present? && collection.present? # Previous
    end
  end

  # Serializes the cursor to send it to the client.
  #
  # @param collection [Enumerable] The collection to analyze.
  # @param page [String] The page to return. It can be `first`, `next`, `prev` or `previous`.
  # @param field [Symbol] When not using offset based pagination, the field to consider for generation.
  # @param size [Fixnum] The number of results to advance when using offset based pagination.
  # @param use_offset [Boolean] Whether to use offset based pagination.
  # @return [String] The serialized cursor.
  def save(collection, page, field: :id, size: nil, use_offset: nil)
    size ||= self.size
    use_offset = self.use_offset if use_offset.nil?
    direction, value = use_offset ? update_with_offset(page, size) : update_with_field(page, collection, field)

    value = value.strftime(TIMESTAMP_FORMAT) if value.respond_to?(:strftime)

    JWT.encode({aud: "pagination", sub: {value: value, use_offset: use_offset, direction: direction, size: size}}, jwt_secret, "HS256")
  end
  alias_method :serialize, :save

  private

  # :nodoc:
  def default_payload
    @value = nil
    @direction = "next"
    @size = 0
    @use_offset = false
  end

  # :nodoc:
  def extract_payload(payload)
    @value = payload["value"]
    @direction = payload["direction"]
    @size = payload["size"]
    @use_offset = payload["use_offset"]
  end

  # :nodoc:
  def sanitize(count_field, params)
    @direction = "next" unless ["prev", "previous"].include?(@direction)
    @size = params[count_field].to_integer if params[count_field].present?
    @size = DEFAULT_SIZE if @size < 1
  end

  # :nodoc:
  def update_with_field(type, collection, field)
    case type.ensure_string
    when "next"
      direction = "next"
      value = collection.last&.send(field)
    when "prev", "previous"
      direction = "previous"
      value = collection.first&.send(field)
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  # :nodoc:
  def update_with_offset(type, size)
    case type.ensure_string
    when "next"
      direction = "next"
      value = self.value + size
    when "prev", "previous"
      direction = "previous"
      value = [0, self.value - size].max
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  def jwt_secret
    Apes::RuntimeConfiguration.jwt_token
  end
end

- (String) value

Returns The value obtain from previous pagination. It can either be the value of the first or last element in previous iteration.

Returns:

  • (String)

    The value obtain from previous pagination. It can either be the value of the first or last element in previous iteration.



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
42
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/apes/pagination_cursor.rb', line 17

class PaginationCursor
  # The default size of a pagination page.
  DEFAULT_SIZE = 25

  # Format to serialize timestamp when using them for pagination.
  TIMESTAMP_FORMAT = "%FT%T.%6N%z".freeze

  attr_accessor :value, :use_offset, :direction, :size

  # Creates a new cursor.
  #
  # @param params [Hash] The request parameters.
  # @param field [Symbol] The parameters field where to lookup for the serialized cursor.
  # @param count_field [Symbol] The parameters field where to lookup for the overriding cursor size.
  # @return [Apes::PaginationCursor] A new cursor instance.
  def initialize(params = {}, field = :page, count_field = :count)
    begin
      payload = JWT.decode(params[field], jwt_secret, true, {algorithm: "HS256", verify_aud: true, aud: "pagination"}).dig(0, "sub")

      extract_payload(payload)
    rescue
      default_payload
    end

    # Sanitization
    sanitize(count_field, params)
  end

  # Get the operator (`>` or `<`) for the query according to the direction and the provided ordering.
  #
  # @param order [Symbol] The order to use.
  # @return [String] The operator to use for the query.
  def operator(order)
    if direction == "next"
      order == :asc ? ">" : "<" # Descending order means newer results first
    else
      order == :asc ? "<" : ">" # Descending order means newer results first
    end
  end

  # Verifies whether a specific page might exist for the given collection.
  #
  # @param page [String] The page to check. It can be `first`, `next`, `prev` or `previous`.
  # @param collection [Enumerable] The collection to analyze.
  # @return [Boolean] Returns `true` if the page might exist for the collection, `false` otherwise.
  def might_exist?(page, collection)
    case page.to_s
    when "first" then true
    when "next" then collection.present?
    else value.present? && collection.present? # Previous
    end
  end

  # Serializes the cursor to send it to the client.
  #
  # @param collection [Enumerable] The collection to analyze.
  # @param page [String] The page to return. It can be `first`, `next`, `prev` or `previous`.
  # @param field [Symbol] When not using offset based pagination, the field to consider for generation.
  # @param size [Fixnum] The number of results to advance when using offset based pagination.
  # @param use_offset [Boolean] Whether to use offset based pagination.
  # @return [String] The serialized cursor.
  def save(collection, page, field: :id, size: nil, use_offset: nil)
    size ||= self.size
    use_offset = self.use_offset if use_offset.nil?
    direction, value = use_offset ? update_with_offset(page, size) : update_with_field(page, collection, field)

    value = value.strftime(TIMESTAMP_FORMAT) if value.respond_to?(:strftime)

    JWT.encode({aud: "pagination", sub: {value: value, use_offset: use_offset, direction: direction, size: size}}, jwt_secret, "HS256")
  end
  alias_method :serialize, :save

  private

  # :nodoc:
  def default_payload
    @value = nil
    @direction = "next"
    @size = 0
    @use_offset = false
  end

  # :nodoc:
  def extract_payload(payload)
    @value = payload["value"]
    @direction = payload["direction"]
    @size = payload["size"]
    @use_offset = payload["use_offset"]
  end

  # :nodoc:
  def sanitize(count_field, params)
    @direction = "next" unless ["prev", "previous"].include?(@direction)
    @size = params[count_field].to_integer if params[count_field].present?
    @size = DEFAULT_SIZE if @size < 1
  end

  # :nodoc:
  def update_with_field(type, collection, field)
    case type.ensure_string
    when "next"
      direction = "next"
      value = collection.last&.send(field)
    when "prev", "previous"
      direction = "previous"
      value = collection.first&.send(field)
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  # :nodoc:
  def update_with_offset(type, size)
    case type.ensure_string
    when "next"
      direction = "next"
      value = self.value + size
    when "prev", "previous"
      direction = "previous"
      value = [0, self.value - size].max
    else # first
      direction = "next"
      value = nil
    end

    [direction, value]
  end

  def jwt_secret
    Apes::RuntimeConfiguration.jwt_token
  end
end

Instance Method Details

- (Boolean) might_exist?(page, collection)

Verifies whether a specific page might exist for the given collection.

Parameters:

  • page (String)

    The page to check. It can be first, next, prev or previous.

  • collection (Enumerable)

    The collection to analyze.

Returns:

  • (Boolean)

    Returns true if the page might exist for the collection, false otherwise.



62
63
64
65
66
67
68
# File 'lib/apes/pagination_cursor.rb', line 62

def might_exist?(page, collection)
  case page.to_s
  when "first" then true
  when "next" then collection.present?
  else value.present? && collection.present? # Previous
  end
end

- (String) operator(order)

Get the operator (> or <) for the query according to the direction and the provided ordering.

Parameters:

  • order (Symbol)

    The order to use.

Returns:

  • (String)

    The operator to use for the query.



49
50
51
52
53
54
55
# File 'lib/apes/pagination_cursor.rb', line 49

def operator(order)
  if direction == "next"
    order == :asc ? ">" : "<" # Descending order means newer results first
  else
    order == :asc ? "<" : ">" # Descending order means newer results first
  end
end

- (String) save(collection, page, field: :id, size: nil, use_offset: nil) Also known as: serialize

Serializes the cursor to send it to the client.

Parameters:

  • collection (Enumerable)

    The collection to analyze.

  • page (String)

    The page to return. It can be first, next, prev or previous.

  • field (Symbol)

    When not using offset based pagination, the field to consider for generation.

  • size (Fixnum)

    The number of results to advance when using offset based pagination.

  • use_offset (Boolean)

    Whether to use offset based pagination.

Returns:

  • (String)

    The serialized cursor.



78
79
80
81
82
83
84
85
86
# File 'lib/apes/pagination_cursor.rb', line 78

def save(collection, page, field: :id, size: nil, use_offset: nil)
  size ||= self.size
  use_offset = self.use_offset if use_offset.nil?
  direction, value = use_offset ? update_with_offset(page, size) : update_with_field(page, collection, field)

  value = value.strftime(TIMESTAMP_FORMAT) if value.respond_to?(:strftime)

  JWT.encode({aud: "pagination", sub: {value: value, use_offset: use_offset, direction: direction, size: size}}, jwt_secret, "HS256")
end