Welcome to mirror list, hosted at ThFree Co, Russian Federation.

paginator.rb « keyset « pagination « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 2ec4472fcd6a0a059607f076a8a2c7087feaff1a (plain)
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