@ -9,7 +9,7 @@ import (
"github.com/hashicorp/boundary/internal/boundary"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/ refresh token"
"github.com/hashicorp/boundary/internal/ list token"
)
// ListResponse represents the response from the paginated list operation.
@ -27,19 +27,18 @@ type ListResponse[T boundary.Resource] struct {
// that it may be appropriate to wait some time before
// requesting additional pages.
CompleteListing bool
// Refresh Token is the token that the caller can use
// List Token is the token that the caller can use
// to request a new page of items. The items in the
// new page will have been updated more recently
// than all the items in the previous page. This
// field may be empty if there were no results for a
// List call.
RefreshToken * refresh token. Token
ListToken * list token. Token
// DeletedIds contains a list of item IDs that have been
// deleted since the last request for items. This can happen
// both during the initial pagination or when requesting a
// refresh. This is always empty for the initial List call.
// deleted since the last request for items. This can only happen
// during a refresh pagination.
DeletedIds [ ] string
// EstimatedItemCount is an estimate o n exactly how many
// EstimatedItemCount is an estimate o f exactly how many
// items matching the filter function are available. If
// a List call is complete, this number is equal to
// the number of items returned. Otherwise, the
@ -52,14 +51,11 @@ type ListResponse[T boundary.Resource] struct {
// result. Returning an error results in an error being returned from the pagination.
type ListFilterFunc [ T boundary . Resource ] func ( ctx context . Context , item T ) ( bool , error )
// ListItemsFunc returns a slice of T that have been updated since prevPageLastItem.
// If prevPageLastItem is empty, it returns a slice of T starting with the least recently updated.
type ListItemsFunc [ T boundary . Resource ] func ( ctx context . Context , prevPageLastItem T , limit int ) ( [ ] T , error )
// ListRefreshItemsFunc returns a slice of T that have been updated since prevPageLastItem.
// If prevPageLastItem is empty, it returns a slice of T that have been updated since the
// item in the refresh token.
type ListRefreshItemsFunc [ T boundary . Resource ] func ( ctx context . Context , tok * refreshtoken . Token , prevPageLastItem T , limit int ) ( [ ] T , error )
// ListItemsFunc returns a slice of T that are ordered after prevPageLastItem according to
// the implementation of the function. If prevPageLastItem is empty, it should return
// a slice of T from the start, as defined by the function. It also returns the timestamp
// of the DB transaction used to list the items.
type ListItemsFunc [ T boundary . Resource ] func ( ctx context . Context , prevPageLastItem T , limit int ) ( [ ] T , time . Time , error )
// EstimatedCountFunc is used to estimate the total number of items
// available for the resource that is being listed.
@ -67,17 +63,16 @@ type EstimatedCountFunc func(ctx context.Context) (int, error)
// ListDeletedIDsFunc is used to list the IDs of the resources deleted since
// the given timestamp. It returns a slice of IDs and the timestamp of the
// instant in which the slice was created .
// DB transaction used to list the IDs .
type ListDeletedIDsFunc func ( ctx context . Context , since time . Time ) ( [ ] string , time . Time , error )
// List returns a ListResponse. The response will contain at most a
// number of items equal to the pageSize. Items are fetched using the
// listItemsFn and then items are checked using the filterItemFn
// to determine if they should be included in the response.
// The response includes a new refresh token based on the grants and items.
// List returns a ListResponse. The response will contain at most pageSize
// number of items. Items are fetched using the listItemsFn and checked using
// the filterItemFn to determine if they should be included in the response.
// The response includes a new list token used to continue pagination or refresh.
// The estimatedCountFn is used to provide an estimated total number of
// items that can be returned by making additional requests using the provid ed
// refresh token.
// items that can be returned by making additional requests using the return ed
// list token.
func List [ T boundary . Resource ] (
ctx context . Context ,
grantsHash [ ] byte ,
@ -88,23 +83,20 @@ func List[T boundary.Resource](
) ( * ListResponse [ T ] , error ) {
const op = "pagination.List"
if len ( grantsHash ) == 0 {
switch {
case len ( grantsHash ) == 0 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing grants hash" )
}
if pageSize < 1 {
case pageSize < 1 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "page size must be at least 1" )
}
if filterItemFn == nil {
case filterItemFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing filter item callback" )
}
if listItemsFn == nil {
case listItemsFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list items callback" )
}
if estimatedCountFn == nil {
case estimatedCountFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing estimated count callback" )
}
items , completeListing , err := list ( ctx , pageSize , filterItemFn , listItemsFn )
items , completeListing , listTime, err := list ( ctx , pageSize , filterItemFn , listItemsFn )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
@ -115,6 +107,38 @@ func List[T boundary.Resource](
EstimatedItemCount : len ( items ) ,
}
if len ( items ) > 0 {
lastItem := items [ len ( items ) - 1 ]
if completeListing {
// If this is the only page in the pagination, create a
// start refresh token so subsequent requests are informed
// that they need to start a new refresh phase.
resp . ListToken , err = listtoken . NewStartRefresh (
ctx ,
listTime , // Use list time as the create time of the token
lastItem . GetResourceType ( ) ,
grantsHash ,
listTime , // Use list time as the starting point for listing deleted ids
listTime , // Use list time as the lower bound for subsequent refresh
)
if err != nil {
return nil , err
}
} else {
resp . ListToken , err = listtoken . NewPagination (
ctx ,
listTime , // Use list time as the create time of the token
lastItem . GetResourceType ( ) ,
grantsHash ,
lastItem . GetPublicId ( ) ,
lastItem . GetCreateTime ( ) . AsTime ( ) ,
)
if err != nil {
return nil , err
}
}
}
if ! completeListing {
// If this was not a complete listing, get an estimate
// of the total items from the DB.
@ -125,66 +149,201 @@ func List[T boundary.Resource](
}
}
if len ( items ) > 0 {
resp . RefreshToken = refreshtoken . FromResource ( items [ len ( items ) - 1 ] , grantsHash )
return resp , nil
}
// ListPage returns a ListResponse. The response will contain at most pageSize
// number of items. Items are fetched using the listItemsFn and checked using
// the filterItemFn to determine if they should be included in the response.
// Items will be fetched based on the contents of the list token. The list
// token must contain a PaginationToken component.
// The response includes a new list token used to continue pagination or refresh.
// The estimatedCountFn is used to provide an estimated total number of
// items that can be returned by making additional requests using the returned
// list token.
func ListPage [ T boundary . Resource ] (
ctx context . Context ,
grantsHash [ ] byte ,
pageSize int ,
filterItemFn ListFilterFunc [ T ] ,
listItemsFn ListItemsFunc [ T ] ,
estimatedCountFn EstimatedCountFunc ,
tok * listtoken . Token ,
) ( * ListResponse [ T ] , error ) {
const op = "pagination.ListPage"
switch {
case len ( grantsHash ) == 0 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing grants hash" )
case pageSize < 1 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "page size must be at least 1" )
case filterItemFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing filter item callback" )
case listItemsFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list items callback" )
case estimatedCountFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing estimated count callback" )
case tok == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list token" )
}
if _ , ok := tok . Subtype . ( * listtoken . PaginationToken ) ; ! ok {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "token did not have a pagination token component" )
}
items , completeListing , listTime , err := list ( ctx , pageSize , filterItemFn , listItemsFn )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
resp := & ListResponse [ T ] {
Items : items ,
CompleteListing : completeListing ,
ListToken : tok ,
}
resp . EstimatedItemCount , err = estimatedCountFn ( ctx )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
var lastItem boundary . Resource
if len ( items ) > 0 {
lastItem = items [ len ( items ) - 1 ]
}
if err := resp . ListToken . Transition (
ctx ,
completeListing ,
lastItem ,
time . Time { } , // We have no deleted ids time
listTime ,
) ; err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
return resp , nil
}
// ListRefresh returns a ListResponse. The response will contain at most a
// number of items equal to the pageSize. Items are fetched using the
// listRefreshItemsFn and then items are checked using the filterItemFn
// to determine if they should be included in the response.
// The response includes a new refresh token based on the grants and items.
// ListRefresh returns a ListResponse. The response will contain at most pageSize
// number of items. Items are fetched using the listItemsFn and checked using
// the filterItemFn to determine if they should be included in the response.
// Items will be fetched based on the contents of the list token. The list
// token must contain a StartRefreshToken component.
// The response includes a new list token used to continue pagination or refresh.
// The estimatedCountFn is used to provide an estimated total number of
// items that can be returned by making additional requests using the provided
// refresh token. The listDeletedIDsFn is used to list the IDs of any
// resources that have been deleted since the refresh token was last used.
// items that can be returned by making additional requests using the return ed
// list token. The listDeletedIDsFn is used to list the IDs of any
// resources that have been deleted since the list token was last used.
func ListRefresh [ T boundary . Resource ] (
ctx context . Context ,
grantsHash [ ] byte ,
pageSize int ,
filterItemFn ListFilterFunc [ T ] ,
listRefreshItemsFn ListRefreshItemsFunc [ T ] ,
list ItemsFn List ItemsFunc[ T ] ,
estimatedCountFn EstimatedCountFunc ,
listDeletedIDsFn ListDeletedIDsFunc ,
tok * refreshtoken . Token ,
tok * list token. Token ,
) ( * ListResponse [ T ] , error ) {
const op = "pagination.ListRefresh"
if len ( grantsHash ) == 0 {
switch {
case len ( grantsHash ) == 0 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing grants hash" )
}
if pageSize < 1 {
case pageSize < 1 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "page size must be at least 1" )
}
if filterItemFn == nil {
case filterItemFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing filter item callback" )
case listItemsFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list items callback" )
case estimatedCountFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing estimated count callback" )
case listDeletedIDsFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list deleted IDs callback" )
case tok == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list token" )
}
if listRefreshItemsFn == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list refresh items callback" )
srt , ok := tok . Subtype . ( * listtoken . StartRefreshToken )
if ! ok {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "token did not have a start-refresh token component" )
}
if estimatedCountFn == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing estimated count callback" )
deletedIds , deletedIdsTime , err := listDeletedIDsFn ( ctx , srt . PreviousDeletedIdsTime )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
if listDeletedIDsFn == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list deleted IDs callback" )
items , completeListing , listTime , err := list ( ctx , pageSize , filterItemFn , listItemsFn )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
if tok == nil {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing refresh token" )
resp := & ListResponse [ T ] {
Items : items ,
CompleteListing : completeListing ,
DeletedIds : deletedIds ,
ListToken : tok ,
}
deletedIds , transactionTimestamp , err := listDeletedIDsFn ( ctx , tok . UpdatedTime )
resp. EstimatedItemCount , err = estimatedCountFn ( ctx )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
var lastItem boundary . Resource
if len ( items ) > 0 {
lastItem = items [ len ( items ) - 1 ]
}
if err := resp . ListToken . Transition ( ctx , completeListing , lastItem , deletedIdsTime , listTime ) ; err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
return resp , nil
}
// ListRefreshPage returns a ListResponse. The response will contain at most pageSize
// number of items. Items are fetched using the listItemsFn and checked using
// the filterItemFn to determine if they should be included in the response.
// Items will be fetched based on the contents of the list token. The list
// token must contain a RefreshToken component.
// The response includes a new list token used to continue pagination or refresh.
// The estimatedCountFn is used to provide an estimated total number of
// items that can be returned by making additional requests using the returned
// list token. The listDeletedIDsFn is used to list the IDs of any
// resources that have been deleted since the list token was last used.
func ListRefreshPage [ T boundary . Resource ] (
ctx context . Context ,
grantsHash [ ] byte ,
pageSize int ,
filterItemFn ListFilterFunc [ T ] ,
listItemsFn ListItemsFunc [ T ] ,
estimatedCountFn EstimatedCountFunc ,
listDeletedIDsFn ListDeletedIDsFunc ,
tok * listtoken . Token ,
) ( * ListResponse [ T ] , error ) {
const op = "pagination.ListRefreshPage"
switch {
case len ( grantsHash ) == 0 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing grants hash" )
case pageSize < 1 :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "page size must be at least 1" )
case filterItemFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing filter item callback" )
case listItemsFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list items callback" )
case estimatedCountFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing estimated count callback" )
case listDeletedIDsFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list deleted IDs callback" )
case tok == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing list token" )
}
rt , ok := tok . Subtype . ( * listtoken . RefreshToken )
if ! ok {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "token did not have a refresh token component" )
}
listItemsFn := func ( ctx context . Context , prevPageLast T , limit int ) ( [ ] T , error ) {
return listRefreshItemsFn ( ctx , tok , prevPageLast , limit )
deletedIds , deletedIdsTime , err := listDeletedIDsFn ( ctx , rt . PreviousDeletedIdsTime )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
items , completeListing , err := list ( ctx , pageSize , filterItemFn , listItemsFn )
items , completeListing , listTime, err := list ( ctx , pageSize , filterItemFn , listItemsFn )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
@ -193,6 +352,7 @@ func ListRefresh[T boundary.Resource](
Items : items ,
CompleteListing : completeListing ,
DeletedIds : deletedIds ,
ListToken : tok ,
}
resp . EstimatedItemCount , err = estimatedCountFn ( ctx )
@ -200,12 +360,13 @@ func ListRefresh[T boundary.Resource](
return nil , errors . Wrap ( ctx , err , op )
}
var lastItem boundary . Resource
if len ( items ) > 0 {
resp . RefreshToken = tok . RefreshLastItem ( items [ len ( items ) - 1 ] , transactionTimestamp )
} else {
resp . RefreshToken = tok . Refresh ( transactionTimestamp )
lastItem = items [ len ( items ) - 1 ]
}
if err := resp . ListToken . Transition ( ctx , completeListing , lastItem , deletedIdsTime , listTime ) ; err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
return resp , nil
}
@ -214,23 +375,29 @@ func list[T boundary.Resource](
pageSize int ,
filterItemFn ListFilterFunc [ T ] ,
listItemsFn ListItemsFunc [ T ] ,
) ( [ ] T , bool , error ) {
) ( [ ] T , bool , time . Time , error ) {
const op = "pagination.list"
var lastItem T
var firstListTime time . Time
limit := pageSize + 1
items := make ( [ ] T , 0 , limit )
dbLoop :
for {
// Request another page from the DB until we fill the final items
page , err := listItemsFn ( ctx , lastItem , limit )
page , listTime, err := listItemsFn ( ctx , lastItem , limit )
if err != nil {
return nil , false , errors . Wrap ( ctx , err , op )
return nil , false , time . Time { } , errors . Wrap ( ctx , err , op )
}
// Assign the firstListTime once, to ensure we always store the start of listing,
// rather the timestamp of the last listing.
if firstListTime . IsZero ( ) {
firstListTime = listTime
}
for _ , item := range page {
ok , err := filterItemFn ( ctx , item )
if err != nil {
return nil , false , errors . Wrap ( ctx , err , op )
return nil , false , time. Time { } , errors. Wrap ( ctx , err , op )
}
if ok {
items = append ( items , item )
@ -256,5 +423,5 @@ dbLoop:
items = items [ : pageSize ]
}
return items , completeListing , nil
return items , completeListing , firstListTime , nil
}