diff --git a/lib/GitLab/API/v4/Paginator.pm b/lib/GitLab/API/v4/Paginator.pm index b911d6d..20d9927 100644 --- a/lib/GitLab/API/v4/Paginator.pm +++ b/lib/GitLab/API/v4/Paginator.pm @@ -82,6 +82,12 @@ has params => ( default => sub{ {} }, ); +has _next_params => ( + is => 'rw', + isa => HashRef, + default => sub{ {} }, +); + =head1 METHODS =cut @@ -110,6 +116,9 @@ 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 { @@ -117,18 +126,41 @@ sub next_page { 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, ); @@ -136,7 +168,11 @@ sub next_page { 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 ] ); @@ -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()) { ... } @@ -207,6 +268,7 @@ sub reset { my ($self) = @_; $self->_records( [] ); $self->_page( 0 ); + $self->_next_params( {} ); $self->_last_page( 0 ); return; } diff --git a/lib/GitLab/API/v4/RESTClient.pm b/lib/GitLab/API/v4/RESTClient.pm index 62be391..319aac9 100644 --- a/lib/GitLab/API/v4/RESTClient.pm +++ b/lib/GitLab/API/v4/RESTClient.pm @@ -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; @@ -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 ]; @@ -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(