api: add automatic list pagination
Updates all List endpoints to automatically paginate through results to preserve backwards compatibility. Adding support for user controllable pagination could be considered at a later time.
This commit is contained in:
1 parent
9ce5a14ef0
commit
ef3bdfd5eb
39 files changed
+2139
-41
No files matched your search
@@ -25,6 +25,7 @@ type options struct {
|
||||
withAutomaticVersioning bool
|
||||
withSkipCurlOutput bool
|
||||
withFilter string
|
||||
withRefreshToken string
|
||||
withRecursive bool
|
||||
}
|
||||
|
||||
@@ -49,6 +50,9 @@ func getOpts(opt ...Option) (options, []api.Option) {
|
||||
if opts.withFilter != "" {
|
||||
opts.queryMap["filter"] = opts.withFilter
|
||||
}
|
||||
if opts.withRefreshToken != "" {
|
||||
opts.queryMap["refresh_token"] = opts.withRefreshToken
|
||||
}
|
||||
if opts.withRecursive {
|
||||
opts.queryMap["recursive"] = strconv.FormatBool(opts.withRecursive)
|
||||
}
|
||||
@@ -63,6 +67,14 @@ func WithSkipCurlOutput(skip bool) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRefreshToken tells the API to use the provided refresh token
|
||||
// for listing operations on this resource.
|
||||
func WithRefreshToken(refreshToken string) Option {
|
||||
return func(o *options) {
|
||||
o.withRefreshToken = refreshToken
|
||||
}
|
||||
}
|
||||
|
||||
// WithRecursive tells the API to use recursion for listing operations on this
|
||||
// resource
|
||||
func WithRecursive(recurse bool) Option {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
@@ -52,14 +53,34 @@ func (n SessionRecordingReadResult) GetResponse() *api.Response {
|
||||
}
|
||||
|
||||
type SessionRecordingListResult struct {
|
||||
Items []*SessionRecording
|
||||
response *api.Response
|
||||
Items []*SessionRecording `json:"items,omitempty"`
|
||||
EstItemCount uint `json:"est_item_count,omitempty"`
|
||||
RemovedIds []string `json:"removed_ids,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ResponseType string `json:"response_type,omitempty"`
|
||||
response *api.Response
|
||||
}
|
||||
|
||||
func (n SessionRecordingListResult) GetItems() []*SessionRecording {
|
||||
return n.Items
|
||||
}
|
||||
|
||||
func (n SessionRecordingListResult) GetEstItemCount() uint {
|
||||
return n.EstItemCount
|
||||
}
|
||||
|
||||
func (n SessionRecordingListResult) GetRemovedIds() []string {
|
||||
return n.RemovedIds
|
||||
}
|
||||
|
||||
func (n SessionRecordingListResult) GetRefreshToken() string {
|
||||
return n.RefreshToken
|
||||
}
|
||||
|
||||
func (n SessionRecordingListResult) GetResponseType() string {
|
||||
return n.ResponseType
|
||||
}
|
||||
|
||||
func (n SessionRecordingListResult) GetResponse() *api.Response {
|
||||
return n.response
|
||||
}
|
||||
@@ -161,5 +182,77 @@ func (c *Client) List(ctx context.Context, scopeId string, opt ...Option) (*Sess
|
||||
return nil, apiErr
|
||||
}
|
||||
target.response = resp
|
||||
if target.ResponseType == "complete" || target.ResponseType == "" {
|
||||
return target, nil
|
||||
}
|
||||
// If there are more results, automatically fetch the rest of the results.
|
||||
// idToIndex keeps a map from the ID of an item to its index in target.Items.
|
||||
// This is used to update updated items in-place and remove deleted items
|
||||
// from the result after pagination is done.
|
||||
idToIndex := map[string]int{}
|
||||
for i, item := range target.Items {
|
||||
idToIndex[item.Id] = i
|
||||
}
|
||||
for {
|
||||
req, err := c.client.NewRequest(ctx, "GET", "session-recordings", nil, apiOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating List request: %w", err)
|
||||
}
|
||||
|
||||
opts.queryMap["refresh_token"] = target.RefreshToken
|
||||
if len(opts.queryMap) > 0 {
|
||||
q := url.Values{}
|
||||
for k, v := range opts.queryMap {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error performing client request during List call: %w", err)
|
||||
}
|
||||
|
||||
page := new(SessionRecordingListResult)
|
||||
apiErr, err := resp.Decode(page)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding List response: %w", err)
|
||||
}
|
||||
if apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
for _, item := range page.Items {
|
||||
if i, ok := idToIndex[item.Id]; ok {
|
||||
// Item has already been seen at index i, update in-place
|
||||
target.Items[i] = item
|
||||
} else {
|
||||
target.Items = append(target.Items, item)
|
||||
idToIndex[item.Id] = len(target.Items) - 1
|
||||
}
|
||||
}
|
||||
target.RemovedIds = append(target.RemovedIds, page.RemovedIds...)
|
||||
target.EstItemCount = page.EstItemCount
|
||||
target.RefreshToken = page.RefreshToken
|
||||
target.ResponseType = page.ResponseType
|
||||
target.response = resp
|
||||
if target.ResponseType == "complete" {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, removedId := range target.RemovedIds {
|
||||
if i, ok := idToIndex[removedId]; ok {
|
||||
// Remove the item at index i without preserving order
|
||||
// https://github.com/golang/go/wiki/SliceTricks#delete-without-preserving-order
|
||||
target.Items[i] = target.Items[len(target.Items)-1]
|
||||
target.Items = target.Items[:len(target.Items)-1]
|
||||
// Update the index of the last element
|
||||
idToIndex[target.Items[i].Id] = i
|
||||
}
|
||||
}
|
||||
// Finally, sort the results again since in-place updates and deletes
|
||||
// may have shuffled items.
|
||||
slices.SortFunc(target.Items, func(i, j *SessionRecording) int {
|
||||
return i.UpdatedTime.Compare(j.UpdatedTime)
|
||||
})
|
||||
return target, nil
|
||||
}
|
||||
Reference in new issue
Block a user