diff options
Diffstat (limited to 'lib/api/helpers/pagination.rb')
-rw-r--r-- | lib/api/helpers/pagination.rb | 249 |
1 files changed, 1 insertions, 248 deletions
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index 71bbc218f94..9c5b355e823 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -4,254 +4,7 @@ module API module Helpers module Pagination def paginate(relation) - strategy = if params[:pagination] == 'keyset' && Feature.enabled?('api_keyset_pagination') - KeysetPaginationStrategy - else - DefaultPaginationStrategy - end - - strategy.new(self).paginate(relation) - end - - class Base - private - - def per_page - @per_page ||= params[:per_page] - end - - def base_request_uri - @base_request_uri ||= URI.parse(request.url).tap do |uri| - uri.host = Gitlab.config.gitlab.host - uri.port = Gitlab.config.gitlab.port - end - end - - def build_page_url(query_params:) - base_request_uri.tap do |uri| - uri.query = query_params - end.to_s - end - - def page_href(next_page_params = {}) - query_params = params.merge(**next_page_params, per_page: per_page).to_query - - build_page_url(query_params: query_params) - end - end - - class KeysetPaginationInfo - attr_reader :relation, :request_context - - def initialize(relation, request_context) - # This is because it's rather complex to support multiple values with possibly different sort directions - # (and we don't need this in the API) - if relation.order_values.size > 1 - raise "Pagination only supports ordering by a single column." \ - "The following columns were given: #{relation.order_values.map { |v| v.expr.name }}" - end - - @relation = relation - @request_context = request_context - end - - def fields - keys.zip(values).reject { |_, v| v.nil? }.to_h - end - - def column_for_order_by(relation) - relation.order_values.first&.expr&.name - end - - # Sort direction (`:asc` or `:desc`) - def sort - @sort ||= if order_by_primary_key? - # Default order is by id DESC - :desc - else - # API defaults to DESC order if param `sort` not present - request_context.params[:sort]&.to_sym || :desc - end - end - - # Do we only sort by primary key? - def order_by_primary_key? - keys.size == 1 && keys.first == primary_key - end - - def primary_key - relation.model.primary_key.to_sym - end - - def sort_ascending? - sort == :asc - end - - # Build hash of request parameters for a given record (relevant to pagination) - def params_for(record) - return {} unless record - - keys.each_with_object({}) do |key, h| - h["ks_prev_#{key}".to_sym] = record.attributes[key.to_s] - end - end - - private - - # All values present in request parameters that correspond to #keys. - def values - @values ||= keys.map do |key| - request_context.params["ks_prev_#{key}".to_sym] - end - end - - # All keys relevant to pagination. - # This always includes the primary key. Optionally, the `order_by` key is prepended. - def keys - @keys ||= [column_for_order_by(relation), primary_key].compact.uniq - end - end - - class KeysetPaginationStrategy < Base - attr_reader :request_context - delegate :params, :header, :request, to: :request_context - - def initialize(request_context) - @request_context = request_context - end - - # rubocop: disable CodeReuse/ActiveRecord - def paginate(relation) - pagination = KeysetPaginationInfo.new(relation, request_context) - - paged_relation = relation.limit(per_page) - - if conds = conditions(pagination) - paged_relation = paged_relation.where(*conds) - end - - # In all cases: sort by primary key (possibly in addition to another sort column) - paged_relation = paged_relation.order(pagination.primary_key => pagination.sort) - - add_default_pagination_headers - - if last_record = paged_relation.last - next_page_params = pagination.params_for(last_record) - add_navigation_links(next_page_params) - end - - paged_relation - end - # rubocop: enable CodeReuse/ActiveRecord - - private - - def conditions(pagination) - fields = pagination.fields - - return if fields.empty? - - placeholder = fields.map { '?' } - - comp = if pagination.sort_ascending? - '>' - else - '<' - end - - [ - # Row value comparison: - # (A, B) < (a, b) <=> (A < a) OR (A = a AND B < b) - # <=> A <= a AND ((A < a) OR (A = a AND B < b)) - "(#{fields.keys.join(',')}) #{comp} (#{placeholder.join(',')})", - *fields.values - ] - end - - def add_default_pagination_headers - header 'X-Per-Page', per_page.to_s - end - - def add_navigation_links(next_page_params) - header 'X-Next-Page', page_href(next_page_params) - header 'Link', link_for('next', next_page_params) - end - - def link_for(rel, next_page_params) - %(<#{page_href(next_page_params)}>; rel="#{rel}") - end - end - - class DefaultPaginationStrategy < Base - attr_reader :request_context - delegate :params, :header, :request, to: :request_context - - def initialize(request_context) - @request_context = request_context - end - - def paginate(relation) - paginate_with_limit_optimization(add_default_order(relation)).tap do |data| - add_pagination_headers(data) - end - end - - private - - def paginate_with_limit_optimization(relation) - pagination_data = relation.page(params[:page]).per(params[:per_page]) - return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation) - return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit) - - limited_total_count = pagination_data.total_count_with_limit - if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT - # The call to `total_count_with_limit` memoizes `@arel` because of a call to `references_eager_loaded_tables?` - # We need to call `reset` because `without_count` relies on `@arel` being unmemoized - pagination_data.reset.without_count - else - pagination_data - end - end - - def add_default_order(relation) - if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty? - relation = relation.order(:id) # rubocop: disable CodeReuse/ActiveRecord - end - - relation - end - - def add_pagination_headers(paginated_data) - header 'X-Per-Page', paginated_data.limit_value.to_s - header 'X-Page', paginated_data.current_page.to_s - header 'X-Next-Page', paginated_data.next_page.to_s - header 'X-Prev-Page', paginated_data.prev_page.to_s - header 'Link', pagination_links(paginated_data) - - return if data_without_counts?(paginated_data) - - header 'X-Total', paginated_data.total_count.to_s - header 'X-Total-Pages', total_pages(paginated_data).to_s - end - - def pagination_links(paginated_data) - [].tap do |links| - links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page - links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page - links << %(<#{page_href(page: 1)}>; rel="first") - - links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data) - end.join(', ') - end - - def total_pages(paginated_data) - # Ensure there is in total at least 1 page - [paginated_data.total_pages, 1].max - end - - def data_without_counts?(paginated_data) - paginated_data.is_a?(Kaminari::PaginatableWithoutCount) - end + ::Gitlab::Pagination::OffsetPagination.new(self).paginate(relation) end end end |