Private/Invoke-DMPagedRequest.ps1

function Invoke-DMPagedRequest {
    <#
    .SYNOPSIS
        Retrieves all pages of a collection resource from the OceanStor REST API.
 
    .DESCRIPTION
        Calls Invoke-DeviceManager in a loop, appending range=[start-end] query parameters
        until the API returns fewer results than the requested page size, indicating the last
        page has been reached. Returns the combined raw data array from all pages.
 
        The end boundary is exclusive (confirmed against a live array: range=[0-99] with
        PageSize 100 returned exactly 99 records, not 100), so each page requests
        [start, start+PageSize) rather than a closed [start, start+PageSize-1] interval.
 
        The OceanStor API returns at most PageSize objects per request when no range is
        specified, silently truncating larger collections. This helper ensures every object
        in a collection is retrieved regardless of size.
 
        If the array rejects the range parameter outright (error code 50331651, "The entered
        parameter is incorrect") on the first page, this falls back to a single unpaged request
        and warns, since an unpaged response on some firmware silently truncates to PageSize
        instead of erroring -- there is no way to distinguish "returned everything" from
        "silently truncated" from the response alone.
 
    .PARAMETER WebSession
        Optional session returned by Connect-deviceManager. The module's cached $script:CurrentOceanstorSession
        session is used when omitted.
 
    .PARAMETER Resource
        REST resource path to page through. If the path already contains query parameters
        (i.e. contains '?'), range parameters are appended with '&'; otherwise '?' is used.
 
    .PARAMETER PageSize
        Number of results to request per page. Defaults to 100, the OceanStor maximum per
        single request.
 
    .PARAMETER TimeoutSec
        Maximum seconds to wait for each REST request page before failing. Defaults to 30
        so a non-responsive endpoint cannot hang validation runs indefinitely.
 
    .PARAMETER ApiV2
        Routes the request through the /api/v2/ endpoint instead of the default
        /deviceManager/rest/ path, forwarded as-is to Invoke-DeviceManager on every page
        and on the unpaged fallback request.
    #>

    param(
        [pscustomobject]$WebSession,

        [Parameter(Mandatory)]
        [string]$Resource,

        [ValidateRange(1, 100)]
        [int]$PageSize = 100,

        [ValidateRange(1, 3600)]
        [int]$TimeoutSec = 30,

        [switch]$ApiV2
    )

    function Get-DMPageSignature {
        param([object[]]$Page)

        if ($Page.Count -eq 0) {
            return 'count=0'
        }

        $first = $Page[0] | ConvertTo-Json -Depth 8 -Compress
        $last = $Page[$Page.Count - 1] | ConvertTo-Json -Depth 8 -Compress
        return "count=$($Page.Count);first=$first;last=$last"
    }

    $allData = [System.Collections.Generic.List[object]]::new()
    $start = 0
    $separator = if ($Resource -match '\?') { '&' } else { '?' }
    $previousPageSignature = $null

    do {
        $end = $start + $PageSize
        $pagedResource = "${Resource}${separator}range=[$start-$end]"

        $response = Invoke-DeviceManager -WebSession $WebSession -Method 'GET' -Resource $pagedResource -TimeoutSec $TimeoutSec -ApiV2:$ApiV2
        $errorProperty = if ($null -ne $response) { $response.PSObject.Properties['error'] } else { $null }
        if ($null -ne $errorProperty -and $errorProperty.Value.Code -ne 0) {
            if ($start -eq 0 -and $errorProperty.Value.Code -eq 50331651) {
                Write-Warning "Resource '$Resource' rejected the range parameter (code 50331651); falling back to a single unpaged request. If the collection exceeds $PageSize items and the array's unpaged endpoint truncates rather than erroring, this may silently return an incomplete result."
                $response = Invoke-DeviceManager -WebSession $WebSession -Method 'GET' -Resource $Resource -TimeoutSec $TimeoutSec -ApiV2:$ApiV2
                $errorProperty = if ($null -ne $response) { $response.PSObject.Properties['error'] } else { $null }
                if ($null -eq $errorProperty -or $errorProperty.Value.Code -eq 0) {
                    $dataProperty = if ($null -ne $response) { $response.PSObject.Properties['data'] } else { $null }
                    $fallbackPage = if ($null -ne $dataProperty) { @($dataProperty.Value) } else { @() }
                    return $fallbackPage
                }
            }
            throw (Get-DMApiErrorMessage -Code $errorProperty.Value.Code -Description $errorProperty.Value.description -ResourceContext $pagedResource)
        }
        $dataProperty = if ($null -ne $response) { $response.PSObject.Properties['data'] } else { $null }
        $page = if ($null -ne $dataProperty) { @($dataProperty.Value) } else { @() }
        $pageSignature = Get-DMPageSignature -Page $page
        if ($start -gt 0 -and $page.Count -eq $PageSize -and $pageSignature -eq $previousPageSignature) {
            throw "Resource '$Resource' returned an identical full page for range starting at $start. The endpoint may be ignoring the range parameter, so paging stopped to avoid an infinite validation run."
        }

        foreach ($item in $page) {
            $allData.Add($item)
        }

        $previousPageSignature = $pageSignature
        $start += $PageSize
    } while ($page.Count -eq $PageSize)

    return $allData.ToArray()
}