summaryrefslogtreecommitdiff
path: root/lib/gitlab/pagination
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/pagination')
-rw-r--r--lib/gitlab/pagination/keyset/header_builder.rb1
-rw-r--r--lib/gitlab/pagination/keyset/paginator.rb176
-rw-r--r--lib/gitlab/pagination/keyset/simple_order_builder.rb2
3 files changed, 178 insertions, 1 deletions
diff --git a/lib/gitlab/pagination/keyset/header_builder.rb b/lib/gitlab/pagination/keyset/header_builder.rb
index 69c468207f6..888d93d5fe3 100644
--- a/lib/gitlab/pagination/keyset/header_builder.rb
+++ b/lib/gitlab/pagination/keyset/header_builder.rb
@@ -13,7 +13,6 @@ module Gitlab
def add_next_page_header(query_params)
link = next_page_link(page_href(query_params))
- header('Links', link)
header('Link', link)
end
diff --git a/lib/gitlab/pagination/keyset/paginator.rb b/lib/gitlab/pagination/keyset/paginator.rb
new file mode 100644
index 00000000000..2ec4472fcd6
--- /dev/null
+++ b/lib/gitlab/pagination/keyset/paginator.rb
@@ -0,0 +1,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
diff --git a/lib/gitlab/pagination/keyset/simple_order_builder.rb b/lib/gitlab/pagination/keyset/simple_order_builder.rb
index 5ac5737c3be..76d6bbadaa4 100644
--- a/lib/gitlab/pagination/keyset/simple_order_builder.rb
+++ b/lib/gitlab/pagination/keyset/simple_order_builder.rb
@@ -26,6 +26,8 @@ module Gitlab
def build
order = if order_values.empty?
primary_key_descending_order
+ elsif Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)
+ Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
elsif ordered_by_primary_key?
primary_key_order
elsif ordered_by_other_column?