1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
# frozen_string_literal: true
module Gitlab
module Pagination
module Keyset
class Paginator
include Enumerable
module Base64CursorConverter
def self.dump(cursor_attributes)
Base64.urlsafe_encode64(Gitlab::Json.dump(cursor_attributes))
end
def self.parse(cursor)
Gitlab::Json.parse(Base64.urlsafe_decode64(cursor)).with_indifferent_access
end
end
FORWARD_DIRECTION = 'n'
BACKWARD_DIRECTION = 'p'
UnsupportedScopeOrder = Class.new(StandardError)
# scope - ActiveRecord::Relation object with order by clause
# cursor - Encoded cursor attributes as String. Empty value will requests the first page.
# per_page - Number of items per page.
# cursor_converter - Object that serializes and de-serializes the cursor attributes. Implements dump and parse methods.
# direction_key - Symbol that will be the hash key of the direction within the cursor. (default: _kd => keyset direction)
def initialize(scope:, cursor: nil, per_page: 20, cursor_converter: Base64CursorConverter, direction_key: :_kd)
@keyset_scope = build_scope(scope)
@order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(@keyset_scope)
@per_page = per_page
@cursor_converter = cursor_converter
@direction_key = direction_key
@has_another_page = false
@at_last_page = false
@at_first_page = false
@cursor_attributes = decode_cursor_attributes(cursor)
set_pagination_helper_flags!
end
# rubocop: disable CodeReuse/ActiveRecord
def records
@records ||= begin
items = if paginate_backward?
reversed_order
.apply_cursor_conditions(keyset_scope, cursor_attributes)
.reorder(reversed_order)
.limit(per_page_plus_one)
.to_a
else
order
.apply_cursor_conditions(keyset_scope, cursor_attributes)
.limit(per_page_plus_one)
.to_a
end
@has_another_page = items.size == per_page_plus_one
items.pop if @has_another_page
items.reverse! if paginate_backward?
items
end
end
# rubocop: enable CodeReuse/ActiveRecord
# This and has_previous_page? methods are direction aware. In case we paginate backwards,
# has_next_page? will mean that we have a previous page.
def has_next_page?
records
if at_last_page?
false
elsif paginate_forward?
@has_another_page
elsif paginate_backward?
true
end
end
def has_previous_page?
records
if at_first_page?
false
elsif paginate_backward?
@has_another_page
elsif paginate_forward?
true
end
end
def cursor_for_next_page
if has_next_page?
data = order.cursor_attributes_for_node(records.last)
data[direction_key] = FORWARD_DIRECTION
cursor_converter.dump(data)
else
nil
end
end
def cursor_for_previous_page
if has_previous_page?
data = order.cursor_attributes_for_node(records.first)
data[direction_key] = BACKWARD_DIRECTION
cursor_converter.dump(data)
end
end
def cursor_for_first_page
cursor_converter.dump({ direction_key => FORWARD_DIRECTION })
end
def cursor_for_last_page
cursor_converter.dump({ direction_key => BACKWARD_DIRECTION })
end
delegate :each, :empty?, :any?, to: :records
private
attr_reader :keyset_scope, :order, :per_page, :cursor_converter, :direction_key, :cursor_attributes
delegate :reversed_order, to: :order
def at_last_page?
@at_last_page
end
def at_first_page?
@at_first_page
end
def per_page_plus_one
per_page + 1
end
def decode_cursor_attributes(cursor)
cursor.blank? ? {} : cursor_converter.parse(cursor)
end
def set_pagination_helper_flags!
@direction = cursor_attributes.delete(direction_key.to_s)
if cursor_attributes.blank? && @direction.blank?
@at_first_page = true
@direction = FORWARD_DIRECTION
elsif cursor_attributes.blank?
if paginate_forward?
@at_first_page = true
else
@at_last_page = true
end
end
end
def paginate_backward?
@direction == BACKWARD_DIRECTION
end
def paginate_forward?
@direction == FORWARD_DIRECTION
end
def build_scope(scope)
keyset_aware_scope, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(scope)
raise(UnsupportedScopeOrder, 'The order on the scope does not support keyset pagination') unless success
keyset_aware_scope
end
end
end
end
end
|