Private/Read-AACGraphQuery.ps1

function Read-AACGraphQuery {
    <#
    .SYNOPSIS
        Reads many Microsoft Graph URIs at once with the Connect-AAC sign-in
        (one Graph token for all of them), each followed through its pages.
    .DESCRIPTION
        -Query maps a name to a Graph URI, or to @{ Uri; MaxItems }. A list
        ('value') is read to the end, following @odata.nextLink (or until
        MaxItems are read); anything else is kept as the single object it
        is. One failing call (no permission, no licence, a 404)
        doesn't stop the others: it is in Errors with Graph's own reason.
 
        Returns @{ Data (name -> the items, or the object); Errors (name ->
        the reason) }.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Query,

        [scriptblock] $OnDone,

        [ValidateRange(1, 16)]
        [int] $ThrottleLimit = 6
    )

    # The script blocks below run inside Invoke-AACHttpBatch, whose own
    # variables ($OnDone, $errors...) would hide ours: every name they use
    # here is prefixed 'graph'.
    $graphLists = @{}; $graphObjects = @{}; $graphOnDone = $OnDone; $graphMax = @{}
    foreach ($name in $Query.Keys) { $graphLists[$name] = [System.Collections.Generic.List[object]]::new() }
    $graphPages = {
        param($Key, [string] $Content)
        $graphPage = if ($Content) { ConvertFrom-Json -InputObject $Content -AsHashtable -Depth 64 } else { @{} }
        if ($graphPage -is [System.Collections.IDictionary] -and $graphPage.Contains('value') -and ($graphPage['value'] -is [System.Collections.IList] -or $null -eq $graphPage['value'])) {
            foreach ($graphItem in @($graphPage['value'])) { if ($null -ne $graphItem) { $graphLists[$Key].Add($graphItem) } }
            $graphEnough = $graphMax.Contains($Key) -and $graphLists[$Key].Count -ge $graphMax[$Key]
            if ($graphPage['@odata.nextLink'] -and -not $graphEnough) { return @{ Uri = [string]$graphPage['@odata.nextLink'] } }
        }
        else { $graphObjects[$Key] = $graphPage }
    }
    # A query is a URI, or @{ Uri; MaxItems } to stop following pages after that many.
    $requests = @(foreach ($name in $Query.Keys) {
            $graphSpec = $Query[$name]
            if ($graphSpec -is [System.Collections.IDictionary]) { if ($graphSpec['MaxItems']) { $graphMax[$name] = [int]$graphSpec['MaxItems'] }; $graphSpec = $graphSpec['Uri'] }
            @{ Key = $name; Body = $null; Uri = [string]$graphSpec }
        })
    $failures = Invoke-AACHttpBatch -Request $requests -OnResponse $graphPages -Resource 'https://graph.microsoft.com' -ThrottleLimit $ThrottleLimit -OnDone {
        param($Key, $Failure, $Done, $Total)
        if ($graphOnDone) { & $graphOnDone $Key $Failure $Done $Total }
    }
    $data = @{}; $graphErrors = @{}
    foreach ($name in $Query.Keys) {
        if ($failures[$name]) { $graphErrors[$name] = [string]$failures[$name]; continue }
        $data[$name] = if ($graphObjects.Contains($name)) { $graphObjects[$name] } else { $graphLists[$name].ToArray() }
    }
    @{ Data = $data; Errors = $graphErrors }
}