Private/Invoke-EhGraph.ps1

function Invoke-EhGraph {
    <#
    .SYNOPSIS
        Wrapper de SOLO LECTURA sobre Microsoft Graph con paginación automática.

    .DESCRIPTION
        Todas las funciones del módulo leen el tenant a través de esta única función.
        Solo hace GET: no existe una ruta de escritura en todo el módulo, y un test lo
        verifica. Sigue @odata.nextLink para devolver la colección completa, y respeta
        el throttling (HTTP 429) con Retry-After.

        No maneja autenticación: se apoya en la sesión ya establecida por
        Connect-MgGraph (módulo Microsoft.Graph.Authentication). Así el usuario controla
        con qué identidad y permisos se conecta, y este módulo nunca toca credenciales.

    .PARAMETER Uri
        Ruta relativa de Graph (por ejemplo '/users') o URL absoluta.

    .PARAMETER InvokeCommand
        Solo para pruebas: reemplaza Invoke-MgGraphRequest por un mock. En uso real se
        deja en su valor por defecto.

    .EXAMPLE
        Invoke-EhGraph -Uri '/users?$select=id,displayName'
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[object]])]
    param(
        [Parameter(Mandatory)]
        [string] $Uri,

        [scriptblock] $InvokeCommand
    )

    $base = 'https://graph.microsoft.com/v1.0'
    $next = if ($Uri -match '^https?://') { $Uri } else { "$base$Uri" }

    $llamar = if ($InvokeCommand) {
        $InvokeCommand
    }
    else {
        { param($u) Invoke-MgGraphRequest -Method GET -Uri $u -OutputType PSObject }
    }

    $resultados = [System.Collections.Generic.List[object]]::new()
    while ($next) {
        $intento = 0
        $respuesta = $null
        while ($true) {
            try {
                $respuesta = & $llamar $next
                break
            }
            catch {
                $intento++
                if ($intento -ge 5) { throw }
                Start-Sleep -Seconds ([Math]::Min([Math]::Pow(2, $intento), 30))
            }
        }

        if ($null -ne $respuesta.value) {
            foreach ($item in $respuesta.value) { $resultados.Add($item) }
            $next = $respuesta.'@odata.nextLink'
        }
        else {
            # Respuesta de un solo objeto (no una colección)
            $resultados.Add($respuesta)
            $next = $null
        }
    }
    return $resultados
}