core/Invoke-GraphRequest2.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
Makes GET requests to the Microsoft Graph API with retry logic and pagination handling.

.DESCRIPTION
Invokes HTTP GET requests against the Microsoft Graph API. Handles pagination by automatically
fetching all pages and merging results. Implements retry logic for transient failures (429, 502, 503, 504).

.PARAMETER Uri
The Graph API endpoint path (starting with '/') or full URL.

.PARAMETER Token
The Bearer token for Graph authentication as a SecureString. Falls back to $env:GRAPH_TOKEN.

.PARAMETER Headers
Additional headers to merge into the request.

.EXAMPLE
$result = Invoke-GraphRequest2 -Uri '/beta/deviceManagement/configurationPolicies' -Token $token
#>


function Invoke-GraphRequest2 {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Uri,

        [SecureString]$Token = $null,

        [hashtable]$Headers = @{}
    )

    #region Parameter Validation
    if ($null -eq $Token -and $env:GRAPH_TOKEN) {
        $Token = ConvertTo-SecureString -String $env:GRAPH_TOKEN -AsPlainText -Force
    }
    if ($null -eq $Token) {
        throw 'No token provided and $env:GRAPH_TOKEN is not set'
    }

    if ($Uri -notmatch '^https?://') {
        $Uri = "https://graph.microsoft.com$Uri"
    }
    #endregion Parameter Validation

    #region Retry Loop
    $maxRetries = 5
    $retryCount = 0
    $allResults = [System.Collections.Generic.List[object]]::new()

    do {
        try {
            $requestHeaders = $Headers.Clone()
            $requestHeaders['Content-Type'] = 'application/json'
            $plainToken = ConvertFrom-SecureString -SecureString $Token -AsPlainText
            $requestHeaders['Authorization'] = "Bearer $plainToken"

            Write-Verbose "GET $Uri (attempt $($retryCount + 1))"
            $response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $requestHeaders -ErrorAction Stop

            if ($response.PSObject.Properties['value']) {
                foreach ($item in $response.value) { $allResults.Add($item) }

                if ($response.PSObject.Properties['@odata.nextLink']) {
                    $Uri = $response.'@odata.nextLink'
                    Write-Verbose "Next page: $Uri"
                    continue
                }
                return , $allResults.ToArray()
            }
            else {
                return $response
            }
        }
        catch {
            $ex = $_
            $code = $null
            if ($ex.Exception.psobject.Properties['Response'] -and $ex.Exception.Response) {
                $code = [int]$ex.Exception.Response.StatusCode
            }

            if ($code -in @(429, 502, 503, 504) -and $retryCount -lt $maxRetries) {
                $wait = 10
                if ($code -eq 429 -and $ex.Exception.Response) {
                    $ra = $ex.Exception.Response.Headers.GetValues('Retry-After') | Select-Object -First 1
                    if ($ra) { $wait = [int]$ra }
                }
                $retryCount++
                Write-Warning "Status $code — retrying in $wait s (retry $retryCount of $maxRetries)"
                Start-Sleep -Seconds $wait
                continue
            }

            throw $ex
        }
    } while ($true)
    #endregion Retry Loop
}

Export-ModuleMember -Function Invoke-GraphRequest2