Commit 6cdd2fa2 authored by Mark Chao's avatar Mark Chao

Merge branch 'mmj-cursor-pager' into 'master'

Introduce CursorPager

See merge request gitlab-org/gitlab!68462
parents d48341c4 bdc7a91f
# frozen_string_literal: true
module Gitlab
module Pagination
module Keyset
class CursorBasedRequestContext
attr_reader :request
delegate :params, :header, to: :request
def initialize(request)
@request = request
end
def per_page
params[:per_page]
end
def cursor
params[:cursor]
end
def apply_headers(cursor_for_next_page)
Gitlab::Pagination::Keyset::HeaderBuilder
.new(self)
.add_next_page_header({ cursor: cursor_for_next_page })
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Pagination
module Keyset
class CursorPager < Gitlab::Pagination::Base
attr_reader :cursor_based_request_context, :paginator
def initialize(cursor_based_request_context)
@cursor_based_request_context = cursor_based_request_context
end
def paginate(relation)
@paginator ||= relation.keyset_paginate(
per_page: cursor_based_request_context.per_page,
cursor: cursor_based_request_context.cursor
)
paginator.records
end
def finalize(_records = [])
# can be called only after executing `paginate(relation)`
apply_headers
end
private
def apply_headers
return unless paginator.has_next_page?
cursor_based_request_context
.apply_headers(paginator.cursor_for_next_page)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Pagination::Keyset::CursorBasedRequestContext do
let(:params) { { per_page: 2, cursor: 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==' } }
let(:request) { double('request', params: params) }
describe '#per_page' do
subject(:per_page) { described_class.new(request).per_page }
it { is_expected.to eq 2 }
end
describe '#cursor' do
subject(:cursor) { described_class.new(request).cursor }
it { is_expected.to eq 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==' }
end
describe '#apply_headers' do
let(:request) { double('request', url: "http://#{Gitlab.config.gitlab.host}/api/v4/projects?per_page=3", params: params) }
let(:params) { { per_page: 3 } }
let(:cursor_for_next_page) { 'eyJuYW1lIjoiSDVicCIsImlkIjoiMjgiLCJfa2QiOiJuIn0=' }
subject(:apply_headers) { described_class.new(request).apply_headers(cursor_for_next_page) }
it 'sets Link header with same host/path as the original request' do
orig_uri = URI.parse(request.url)
expect(request).to receive(:header).once do |name, header|
first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures
uri = URI.parse(first_link)
expect(name).to eq('Link')
expect(uri.host).to eq(orig_uri.host)
expect(uri.path).to eq(orig_uri.path)
end
apply_headers
end
it 'sets Link header with a cursor to the next page' do
orig_uri = URI.parse(request.url)
expect(request).to receive(:header).once do |name, header|
first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures
query = CGI.parse(URI.parse(first_link).query)
expect(name).to eq('Link')
expect(query.except('cursor')).to eq(CGI.parse(orig_uri.query).except('cursor'))
expect(query['cursor']).to eq([cursor_for_next_page])
end
apply_headers
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Pagination::Keyset::CursorPager do
let(:relation) { Group.all.order(:name, :id) }
let(:per_page) { 3 }
let(:params) { { cursor: nil, per_page: per_page } }
let(:request) { double('request', params: params) }
let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request) }
before_all do
create_list(:group, 7)
end
describe '#paginate' do
subject(:paginated_result) { described_class.new(cursor_based_request_context).paginate(relation) }
it 'returns the limited relation' do
expect(paginated_result).to eq(relation.limit(per_page))
end
end
describe '#finalize' do
subject(:finalize) do
service = described_class.new(cursor_based_request_context)
# we need to do this because `finalize` can only be called
# after `paginate` is called. Otherwise the `paginator` object won't be set.
service.paginate(relation)
service.finalize
end
it 'passes information about next page to request' do
cursor_for_next_page = relation.keyset_paginate(**params).cursor_for_next_page
expect_next_instance_of(Gitlab::Pagination::Keyset::HeaderBuilder, cursor_based_request_context) do |builder|
expect(builder).to receive(:add_next_page_header).with({ cursor: cursor_for_next_page })
end
finalize
end
context 'when retrieving the last page' do
let(:relation) { Group.where('id > ?', Group.maximum(:id) - per_page).order(:name, :id) }
it 'does not build information about the next page' do
expect(Gitlab::Pagination::Keyset::HeaderBuilder).not_to receive(:new)
finalize
end
end
context 'when retrieving an empty page' do
let(:relation) { Group.where('id > ?', Group.maximum(:id) + 1).order(:name, :id) }
it 'does not build information about the next page' do
expect(Gitlab::Pagination::Keyset::HeaderBuilder).not_to receive(:new)
finalize
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment