Skip to content

Add keyset pagination #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 71 additions & 9 deletions lib/GitLab/API/v4/Paginator.pm
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ has params => (
default => sub{ {} },
);

has _next_params => (
is => 'rw',
isa => HashRef,
default => sub{ {} },
);

=head1 METHODS

=cut
Expand Down Expand Up @@ -110,33 +116,63 @@ has _last_page => (

Returns an array ref of records for the next page.

To use keyset pagination pagination=>'keyset' must be set
in params hash

=cut

sub next_page {
my ($self) = @_;

return if $self->_last_page();

my $page = $self->_page() + 1;
my $page;
my $params = $self->params();
my $per_page = $params->{per_page} || 20;
# Don't allow cursor to be directly passed in
delete $params->{'cursor'};

$params = {
%$params,
page => $page,
per_page => $per_page,
};
# if keyset pagination
my $pagination = $params->{'pagination'};
my $keyset = (defined $pagination && $pagination eq 'keyset') ? 1 : 0;

my $method = $self->method();
my $records = $self->api->$method(
# As of Gitlab v17.0 'users'endpoint only uses keyset pagination
$keyset = 1 if ($method eq 'users' && ! $keyset);

my $per_page = $params->{per_page} || 20;

if ($keyset) {
my $next_params = $self->_next_params();
$params = {
'order_by' => 'id',
'sort' => 'asc',
'per_page' => $per_page,
%$params,
'pagination' => 'keyset',
%$next_params,
};
} else {
$page = $self->_page() + 1;
$params = {
%$params,
page => $page,
per_page => $per_page,
};
}

my ($headers,$records) = $self->api->$method(
@{ $self->args() },
$params,
);

croak("The $method method returned a non array ref value")
if ref($records) ne 'ARRAY';

$self->_page( $page );
if ($keyset) {
$self->_next_link_params($headers)
} else {
$self->_page( $page );
}
$self->_last_page( 1 ) if @$records < $per_page;
$self->_records( [ @$records ] );

Expand All @@ -145,6 +181,31 @@ sub next_page {
return $records;
}

=head2 _next_link_params

Sets _next_params hash, from the params returned in the next link,
when using pagination='keyset'

=cut

sub _next_link_params {
my ($self,$headers) = @_;
my $links = $headers->{'link'};
return undef if ! $links;
return undef if ! ($links =~ m/([^<]*)>; rel="next"/);
my $nextLink = $1;
my $params = {};
my ($url,$paramStr) = split('\?',$nextLink);
return undef if (! $paramStr);
my @paramList = split("&",$paramStr);
foreach my $param (@paramList) {
my ($key,$val) = split("=",$param);
$params->{$key} = $val;
}
$self->_next_params($params);
return 1;
}

=head2 next

while (my $record = $paginator->next()) { ... }
Expand Down Expand Up @@ -207,6 +268,7 @@ sub reset {
my ($self) = @_;
$self->_records( [] );
$self->_page( 0 );
$self->_next_params( {} );
$self->_last_page( 0 );
return;
}
Expand Down
20 changes: 16 additions & 4 deletions lib/GitLab/API/v4/RESTClient.pm
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ sub request {
my $query = delete $options->{query};
my $content = delete $options->{content};
my $headers = $options->{headers} = { %{ $options->{headers} || {} } };
my $pagination = $query->{'pagination'};
# delete cursor and save it for later to avoid it being corrupted
my $cursor = delete $query->{'cursor'};

# Convert foo/:bar/baz into foo/%s/baz.
my $path = $raw_path;
Expand All @@ -163,9 +166,13 @@ sub request {

my $url = $self->_clean_base_url->clone();
$url->path( $url->path() . '/' . $path );
# query_form can corrupt the cursor so we add it on afterwards
$url->query_form( $query ) if defined $query;
$url = "$url"; # No more changes to the url from this point forward.

$url = "$url";
if ($pagination && $cursor) {
$url = "$url&cursor=$cursor";
}
# No more changes to the url from this point forward.
my $req_method = 'request';
my $req = [ $verb, $url, $options ];

Expand Down Expand Up @@ -240,10 +247,15 @@ sub request {

my $decode = $options->{decode};
$decode = 1 if !defined $decode;
return $res->{content} if !$decode;

# returns headers,contents.
# When called in scalar mode, contents is returned and
# headers thrown away.
#
return $res->{'headers'},$res->{content} if !$decode;

return try{
$self->json->decode( $res->{content} );
$res->{'headers'},$self->json->decode( $res->{content} );
}
catch {
croakf(
Expand Down