private/new-GraphBatchQuery.ps1
function New-GraphBatchQuery { <# .SYNOPSIS Executes Microsoft Graph API requests in batch mode with built-in retry logic and throttling handling. .DESCRIPTION This function processes multiple Microsoft Graph API requests either in batches or individually. It implements retry logic for handling throttling (429) errors and other transient failures. The function supports the Graph Batch API for improved performance with large numbers of requests. .PARAMETER BatchItems Array of items to be processed in batches. These could be user objects, groups, or any other entities. .PARAMETER BatchSize Maximum number of items to include in each batch. Default is 20, which is the Graph API limit. .PARAMETER BatchUrlGenerator Script block that generates the URL for each batch item. This script block receives the current item as a parameter. Example: { param($item) return "/users/$($item.id)" } .PARAMETER BatchIdGenerator Script block that generates a unique ID for each request within a batch. Default generates "batch_X" where X is the index. Example: { param($index) return "user_$index" } .PARAMETER ProgressId ID to use for the PowerShell progress bar. Default is 2. .PARAMETER BatchActivity Text to display in the PowerShell progress bar. Default is "Processing batch requests". .PARAMETER Uri Base URI for the Graph API batch endpoint. Defaults to the global batch URL. .PARAMETER maxRetries Maximum number of retry attempts for each request on failed attempts. Default is 3. .EXAMPLE # Process user owned objects in batches # This example retrieves owned objects for all users in batches of 20. $allUsers = New-GraphQuery -Uri "$($global:octo.graphUrl)/v1.0/users?`$select=id,userPrincipalName,displayName" -Method GET $batchOwnedObjectsSplat = @{ batchItems = $allUsers batchSize = 20 batchActivity = "Processing user owned objects" batchUrlGenerator = { param($user) return "/users/$($user.id)/ownedObjects?`$select=id,displayName,groupTypes,mailEnabled,securityEnabled,membershipRule&`$top=999" } batchIdGenerator = { param($index) return "owned_$index" } progressId = 2 } $ownedObjectsBatchResults = new-GraphBatchQuery @batchOwnedObjectsSplat .NOTES Author = "Jos Lieben (jos@lieben.nu)" CompanyName = "Lieben Consultancy" Copyright = "https://www.lieben.nu/liebensraum/commercial-use/" #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [Array]$BatchItems, [Parameter(Mandatory = $false)] [int]$BatchSize = 20, [Parameter(Mandatory = $true)] [scriptblock]$BatchUrlGenerator, [Parameter(Mandatory = $false)] [scriptblock]$BatchIdGenerator = { param($index) return "batch_$index" }, [Parameter(Mandatory = $false)] [int]$ProgressId = 2, [Parameter(Mandatory = $false)] [string]$BatchActivity = "Processing batch requests", [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$Uri = $global:octo.graphbatchUrl, [Parameter(Mandatory = $false)] [int]$maxRetries = 3 ) Write-LogMessage -level 4 -message "Processing $($BatchItems.Count) items in batch mode" $batchApiUrl = "$($global:octo.graphbatchUrl)" $batchResults = @() for ($i = 0; $i -lt $BatchItems.Count; $i += $BatchSize) { $currentBatchSize = [math]::Min($BatchSize, $BatchItems.Count - $i) $currentBatch = $BatchItems[$i..($i + $currentBatchSize - 1)] $progressPercent = [math]::Min(100, ($i / $BatchItems.Count) * 100) Write-Progress -Id $ProgressId -PercentComplete $progressPercent -Activity $BatchActivity -Status "Processing items $i to $($i + $currentBatchSize - 1) of $($BatchItems.Count)" # Create batch requests $batchRequests = @() foreach ($j in 0..($currentBatch.Count - 1)) { $id = & $BatchIdGenerator $j $url = & $BatchUrlGenerator $currentBatch[$j] $batchRequests += @{ id = $id method = "GET" url = $url } } $batchBody = @{"requests" = @($batchRequests) } | ConvertTo-Json -Depth 10 # Execute batch request with retry function $batchResponse = New-GraphQuery -Method POST -Uri $batchApiUrl -Body $batchBody if ($null -eq $batchResponse) { # Create an empty response to maintain indexing $emptyResponse = @{ responses = @(foreach ($req in $batchRequests) { @{ id = $req.id; status = 500; body = $null } }) } $batchResults += $emptyResponse continue } # Process any failed items individually $failedItems = @() foreach ($response in $batchResponse.responses) { if ($response.status -ne 200) { $itemIndex = [int]($response.id -replace "^.*?_(\d+)$", '$1') if ($itemIndex -lt $currentBatch.Count) { $failedItems += @{ item = $currentBatch[$itemIndex] responseId = $response.id originalIndex = $batchResponse.responses.IndexOf($response) } } } } # Retry failed items individually so we do not run the same call more than once if ($failedItems.Count -gt 0) { Write-LogMessage -level 3 -message "Retrying $($failedItems.Count) failed items individually" # Wait a bit before retrying to avoid immediate throttling Start-Sleep -Seconds 2 foreach ($failedItem in $failedItems) { $url = & $BatchUrlGenerator $failedItem.item # Ensure URL is absolute if ($url.StartsWith("/")) { $url = "$($global:octo.graphUrl)/v1.0$url" Write-LogMessage -level 3 -message "Retrying failed item $($failedItem.responseId) with URL: $url" } $retryResult = New-GraphQuery -Uri $url -Method GET # Handle the retry result properly if ($null -ne $retryResult) { # Check if it's our special "resource not found" marker if ($retryResult -is [hashtable] -and $retryResult.ContainsKey('status') -and $retryResult.status -eq 404) { # Update with 404 status but don't crash $batchResponse.responses[$failedItem.originalIndex] = @{ id = $failedItem.responseId status = 404 body = $null error = $retryResult.error } Write-LogMessage -level 2 -message "Resource not found for item $($failedItem.responseId)" } else { # Normal successful retry $batchResponse.responses[$failedItem.originalIndex] = @{ id = $failedItem.responseId status = 200 body = $retryResult } Write-LogMessage -level 4 -message "Successfully retried item $($failedItem.responseId)" } } } } $batchResults += $batchResponse } Write-Progress -Id $ProgressId -Completed -Activity $BatchActivity return $batchResults } |