Private/Get-SIAPagedResult.ps1

function Get-SIAPagedResult {
    <#
    .SYNOPSIS
    Follows SIA API pagination and returns the combined result set.

    .DESCRIPTION
    SIA list endpoints page in one of two ways:
      - Cursor: the response carries an opaque continuation token (nextCursor,
        b64_last_evaluated_key, etc) which is resent verbatim as a query parameter
        until it comes back null/empty.
      - Offset: the response (or a separate count endpoint) carries a total record
        count, and successive pages are requested by incrementing an offset query
        parameter until that many results have been collected.

    Adapted from psPAS's Get-NextLink, which auto-detects nextLink/nextCursor/totalCount
    shapes against a single API convention. SIA has no single convention across endpoints
    (differing cursor/offset/total field and query-parameter names, and one endpoint -
    strong accounts - reports its total via a wholly separate endpoint) so callers here
    are explicit about which fields/keys apply rather than relying on auto-detection.

    .PARAMETER InitialResult
    The already-fetched first page of results.

    .PARAMETER URI
    The URI used for the initial request (without any paging query parameter appended).
    Subsequent pages are requested against this URI.

    .PARAMETER Style
    'Cursor' to follow a continuation token, 'Offset' to page via an incrementing offset
    query parameter against a known/reported total.

    .PARAMETER ResultProperty
    The property on the response holding the array of items. Omit when the response
    itself is a bare array.

    .PARAMETER CursorRequestKey
    Cursor style only. Query parameter name the continuation token is sent back as.

    .PARAMETER CursorResponseKey
    Cursor style only. Property on the response holding the next continuation token.

    .PARAMETER OffsetRequestKey
    Offset style only. Query parameter name used to request the next page.

    .PARAMETER TotalResponseKey
    Offset style only. Property on the response holding the total record count.
    Ignored if TotalCount is supplied.

    .PARAMETER TotalCount
    Offset style only. Use when the total record count is not present on the response
    itself and was instead obtained from a separate request (eg strong accounts'
    /api/secrets/count). Overrides TotalResponseKey.

    .EXAMPLE
    Get-SIAPagedResult -InitialResult $result -URI $URI -Style Cursor -ResultProperty items -CursorResponseKey nextCursor -CursorRequestKey cursor

    .EXAMPLE
    Get-SIAPagedResult -InitialResult $result -URI $URI -Style Offset -ResultProperty items -TotalResponseKey totalCount -OffsetRequestKey offset

    .INPUTS
    None. InitialResult is not accepted from the pipeline - a bare-array API response (eg Get-SIAStrongAccount)
    would otherwise be split into one pipeline call per element instead of a single call with the whole array.

    .OUTPUTS
    All items across all pages.
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory = $true)]
        $InitialResult,

        [parameter(Mandatory = $true)]
        [string]$URI,

        [parameter(Mandatory = $true)]
        [ValidateSet('Cursor', 'Offset')]
        [string]$Style,

        [parameter(Mandatory = $false)]
        [string]$ResultProperty,

        [parameter(Mandatory = $false)]
        [string]$CursorRequestKey = 'cursor',

        [parameter(Mandatory = $false)]
        [string]$CursorResponseKey = 'nextCursor',

        [parameter(Mandatory = $false)]
        [string]$OffsetRequestKey = 'offset',

        [parameter(Mandatory = $false)]
        [string]$TotalResponseKey = 'totalCount',

        [parameter(Mandatory = $false)]
        [int]$TotalCount
    )

    process {

        #Local helper to pull the item array out of a page, whether it's wrapped in a property or a bare array
        $GetItems = {
            param($PageResult)
            if ($ResultProperty) { $PageResult.$ResultProperty } else { $PageResult }
        }

        $Items = [Collections.Generic.List[Object]]::New()

        $InitialItems = & $GetItems $InitialResult
        if ($null -ne $InitialItems) {
            $null = $Items.AddRange(@($InitialItems))
        }

        switch ($Style) {

            'Cursor' {

                $NextCursor = $InitialResult.$CursorResponseKey

                while (-not [String]::IsNullOrEmpty($NextCursor)) {

                    $Separator = if ($URI -match '\?') { '&' } else { '?' }
                    $QueryString = @{ $CursorRequestKey = $NextCursor } | ConvertTo-QueryString
                    $PageURI = "$URI$Separator$QueryString"

                    $PageResult = Invoke-IDRestMethod -Uri $PageURI -Method GET
                    $PageItems = & $GetItems $PageResult

                    if (($null -eq $PageItems) -or (@($PageItems).Count -eq 0)) {
                        break
                    }

                    $null = $Items.AddRange(@($PageItems))
                    $NextCursor = $PageResult.$CursorResponseKey

                }

            }

            'Offset' {

                $Total = if ($PSBoundParameters.ContainsKey('TotalCount')) { $TotalCount } else { $InitialResult.$TotalResponseKey }

                #Tracks the previous page's first item so a server that silently ignores the offset
                #parameter (confirmed for eg SIA's /api/secrets 'count' and /api/monitoring/sessions'
                #offset/limit/pageSize - all three no-op there) is detected and stopped, rather than
                #having the same page appended over and over until Total is reached.
                $PreviousPageFirstItem = if (@($InitialItems).Count -gt 0) { @($InitialItems)[0] | ConvertTo-Json -Compress -Depth 5 } else { $null }

                while (($null -ne $Total) -and ($Items.Count -lt $Total)) {

                    $Separator = if ($URI -match '\?') { '&' } else { '?' }
                    $QueryString = @{ $OffsetRequestKey = $Items.Count } | ConvertTo-QueryString
                    $PageURI = "$URI$Separator$QueryString"

                    $PageResult = Invoke-IDRestMethod -Uri $PageURI -Method GET
                    $PageItems = & $GetItems $PageResult

                    if (($null -eq $PageItems) -or (@($PageItems).Count -eq 0)) {
                        #Defends against a reported total the server never actually delivers
                        #(eg SIA's /api/secrets 'count' parameter is not enforced server-side)
                        break
                    }

                    $CurrentPageFirstItem = @($PageItems)[0] | ConvertTo-Json -Compress -Depth 5

                    if ($CurrentPageFirstItem -eq $PreviousPageFirstItem) {
                        #Offset didn't move the server on - stop rather than duplicate this page's items
                        break
                    }

                    $null = $Items.AddRange(@($PageItems))
                    $PreviousPageFirstItem = $CurrentPageFirstItem

                }

            }

        }

        $Items

    }#process

}