internal/New-GraphBatchRequest.ps1

function New-GraphBatchRequest {
    <#
    .SYNOPSIS
    Function creates PSObject(s) representing request(s) that can be used in Graph Api batching.
 
    .DESCRIPTION
    Function creates PSObject(s) representing request(s) that can be used in Graph Api batching.
 
    PSObject will look like this:
        @{
            Method = "GET"
            URL = "/deviceManagement/managedDevices/38027eb9-1f3e-49ea-bf91-f7b7f07c3a63"
            Id = "deviceInfo"
        }
 
        Method = method that will be used when sending the request
        URL = ARM api URL that should be requested
        Id = ID that has to be unique across the batch requests
 
    .PARAMETER method
    Request method.
 
    Possible values: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'.
 
    By default GET.
 
    .PARAMETER url
    Request URL in relative form like "/deviceManagement/managedDevices/38027eb9-1f3e-49ea-bf91-f7b7f07c3a63" a.k.a. without the "https://graph.microsoft.com/<apiVersion>" prefix (API version is specified when the batch is invoked).
 
    When the 'placeholder' parameter is specified, for each value it contains, new request url will be generated with such value used instead of the '<placeholder>' string.
 
    .PARAMETER placeholder
    Array of items (string, integers, ..) that will be used in the request url ('url' parameter) instead of the "<placeholder>" string.
 
    .PARAMETER header
    Header that should be added to each request in the batch.
 
    .PARAMETER body
    Body that should be added to each request in the batch.
 
    .PARAMETER id
    Id of the request.
    If created request will be invoked via 'Invoke-GraphBatchRequest' function, this Id will be saved in the returned object's 'RequestId' property.
    Can only be specified when 'url' parameter contains just one value.
    If url with placeholder is used, suffix "_<randomnumber>" will be added to each generated request id. This way each one is unique and at the same time you are able to filter the request results based on it in case you merge multiple different requests in one final batch.
 
    Cannot contain "\" character, because Invoke-MgRestMethod used for sending request automatically tries to convert the returned JSON back and it fails because of this special character.
 
    By default random-generated-number.
 
    .PARAMETER placeholderAsId
    Switch to use current 'placeholder' value used in the request URL as an request ID.
 
    BEWARE that request ID has to be unique across the pools of all batch requests, therefore use this switch with a caution!
 
    .EXAMPLE
    $batchRequest = New-GraphBatchRequest -url "/deviceManagement/managedDevices/38027eb9-1f3e-49ea-bf91-f7b7f07c3a63?`$select=id,devicename&`$expand=DetectedApps", "/deviceManagement/managedDevices/aaa932b4-5af4-4120-86b1-ab64b964a56s?`$select=id,devicename&`$expand=DetectedApps"
 
    Invoke-GraphBatchRequest -batchRequest $batchRequest -graphVersion beta
 
    Creates batch request object containing both urls & run it ('DetectedApps' property can be retrieved only when requested devices one by one).
 
    .EXAMPLE
    $deviceId = (Get-MgBetaDeviceManagementManagedDevice -Property id -All).Id
 
    New-GraphBatchRequest -url "/deviceManagement/managedDevices/<placeholder>?`$select=id,devicename&`$expand=DetectedApps" -placeholder $deviceId | Invoke-GraphBatchRequest -graphVersion beta
 
    Creates batch request object containing dynamically generated urls for every id in the $deviceId array & run it ('DetectedApps' property can be retrieved only when requested devices one by one).
 
    .EXAMPLE
    $devices = Get-MgBetaDeviceManagementManagedDevice -Property Id, AzureAdDeviceId, OperatingSystem -All
 
    $windowsClient = $devices | ? OperatingSystem -EQ 'Windows'
 
    $batchRequest = @(
        # get bitlocker keys for all windows devices
        New-GraphBatchRequest -url "/informationProtection/bitlocker/recoveryKeys?`$filter=deviceId eq '<placeholder>'" -id "bitlocker" -placeholder $windowsClient.AzureAdDeviceId
 
        # get LAPS
        New-GraphBatchRequest -url "/directory/deviceLocalCredentials/<placeholder>?`$select=credentials" -id "laps" -placeholder $windowsClient.AzureAdDeviceId
 
        # get all users
        New-GraphBatchRequest -url "/users" -id "users"
    )
 
    $batchResult = Invoke-GraphBatchRequest -batchRequest $batchRequest -graphVersion beta
 
    $bitlockerKeyList = $batchResult | ? RequestId -like "bitlocker*"
    $lapsKeyList = $batchResult | ? RequestId -like "laps*"
    $userList = $batchResult | ? RequestId -eq "users"
 
    Merging multiple different batch queries together.
 
    .EXAMPLE
    $devices = Get-MgBetaDeviceManagementManagedDevice -Property Id, AzureAdDeviceId, OperatingSystem -All
 
    $macOSClient = $devices | ? OperatingSystem -EQ 'macOS'
 
    New-GraphBatchRequest -url "/deviceManagement/managedDevices('<placeholder>')/getFileVaultKey" -placeholderAsId -placeholder $macOSClient.Id | Invoke-GraphBatchRequest -graphVersion beta
 
    Get fileVault keys for all MacOs devices, where returned object's RequestId property will contain Id of the corresponding MacOS device and Value property will contains the key itself.
 
    .EXAMPLE
    $body = @{
        DisplayName= "test"
        MailEnabled= $false
        securityEnabled= $true
        MailNickName= "test"
        description= "test"
    }
 
    $header = @{
        "Content-Type"= "application/json"
    }
 
    New-GraphBatchRequest -method POST -url "/groups/" -body $body -header $header | Invoke-GraphBatchRequest -Verbose
 
    Create specified group.
 
    .NOTES
    Author: @AndrewZtrhgf
 
    HomePage: https://doitpshway.com
 
    HomeModule: MSGraphStuff
 
    https://learn.microsoft.com/en-us/graph/json-batching
    #>


    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [ValidateNotNullOrEmpty()]
        [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')]
        [string] $method = "GET",

        [Parameter(Mandatory = $true)]
        [Alias("urlWithPlaceholder")]
        [string[]] $url,

        $placeholder,

        [hashtable] $header,

        [hashtable] $body,

        [Parameter(ParameterSetName = "Id")]
        [ValidateScript( {
            if ($_ -like "*\*") {
                throw "Id ($_) can't contain '\' character!"
            } else {
                $true
            }
        })]
        [string] $id,

        [Parameter(ParameterSetName = "PlaceholderAsId")]
        [switch] $placeholderAsId
    )

    #region validity checks
    if ($id -and @($url).count -gt 1) {
        throw "'id' parameter cannot be used with multiple urls"
    }

    if ($placeholder -and $url -notlike "*<placeholder>*") {
        throw "You have specified 'placeholder' parameter, but 'url' parameter doesn't contain string '<placeholder>' for replace."
    }

    if (!$placeholder -and $url -like "*<placeholder>*") {
        throw "You have specified 'url' with '<placeholder>' in it, but not the 'placeholder' parameter itself."
    }

    if ($placeholderAsId -and !$placeholder) {
        throw "'placeholderAsId' parameter cannot be used without specifying 'placeholder' parameter"
    }

    if ($placeholderAsId -and $placeholder -and @($url).count -gt 1) {
        throw "'placeholderAsId' parameter cannot be used with multiple urls"
    }

    if ($placeholderAsId) {
        $placeholder | % {
            if ($_ -like "*\*") {
                throw "'placeholderAsId' parameter cannot be used when 'placeholder' contains '\' character (value: '$_')!"
            }
        }
    }

    # method is case sensitive!
    $method = $method.ToUpper()
    #endregion validity checks

    if ($placeholder) {
        $url = $placeholder | % {
            $p = $_

            $url | % {
                $_ -replace "<placeholder>", $p
            }
        }
    }

    $index = 0

    $url | % {
        # fix common mistake where there are multiple following slashes
        $_ = $_ -replace "(?<!^https:)/{2,}", "/"

        if ($_ -like "http*" -or $_ -like "*/beta/*" -or $_ -like "*/v1.0/*" -or $_ -like "*/graph.microsoft.com/*") {
            throw "url '$_' has to be in the relative form (without the whole 'https://graph.microsoft.com/<apiversion>' part)!"
        }

        $property = [ordered]@{
            method = $method
            URL    = $_
        }

        if ($id) {
            if ($placeholder -and $placeholder.count -gt 1) {
                $property.id = ($id + "_" + (Get-Random))
            } else {
                $property.id = $id
            }
        } elseif ($placeholderAsId -and $placeholder) {
            $property.id = @($placeholder)[$index]
        } else {
            $property.id = Get-Random
        }

        if ($header) {
            $property.headers = $header
        }

        if ($body) {
            $property.body = $body
        }

        New-Object -TypeName PSObject -Property $property

        ++$index
    }
}