Private/Invoke-IDNWRestMethod.ps1

<#
    .SYNOPSIS
        Invoke a REST method to the IdentityNow API.

    .DESCRIPTION
        This function is used to invoke a REST method to the IdentityNow API. It will handle pagination and retries.

    .PARAMETER Url
        The URL to call.

    .PARAMETER UrlParams
        The parameters to add to the URL.

    .PARAMETER Method
        The HTTP method to use.

    .PARAMETER Body
        The body of the request.

    .PARAMETER ContentType
        The content type of the request.

    .PARAMETER MaxRetries
        The maximum number of retries to attempt.

    .PARAMETER PauseDuration
        The duration to pause between retries.

    .EXAMPLE
        Invoke-IDNWRestMethod -Url 'https://$($script:IDNWEnv.BaseAPIUrl)/roles' -Method 'GET'

    .INPUTS
        None

    .OUTPUTS
        System.Object[]
#>


function Invoke-IDNWRestMethod {
    [CmdletBinding(
        SupportsShouldProcess = $False,
        ConfirmImpact = "None",
        SupportsPaging = $False,
        PositionalBinding = $True)
    ]
    param (
        [Parameter()]
        [String]
        $Url,

        [Parameter()]
        [Hashtable]
        $UrlParams,

        [Parameter()]
        [ValidateSet("GET", "PATCH", "POST", "PUT", "DELETE")]
        [String]
        $Method = "GET",

        [Parameter()]
        [String]
        $Body,

        [Parameter()]
        [String]
        $ContentType = "application/json",

        [Parameter(Mandatory = $false)]
        [Int]
        $MaxRetries = 3,

        [Parameter(Mandatory = $false)]
        [Int]
        $PauseDuration = 2
    )

    # Check if connected to IdentityNow
    Test-IDNWConnection

    # Create authorization header
    $Headers = @{
        Authorization = ('Bearer {0}' -f $script:IDNWEnv.SessionToken)
    }

    # Add application/json-patch+json content-type for PATCH requests
    if ($Method -eq "PATCH") {
        $ContentType = "application/json-patch+json"
    }

    # Set initial variables
    $SendCall = $true
    $RetryCount = 0
    $AllResults = @()
    $i = 0

    # Add count parameter to the UrlParams to be able to do pagination
    if ($UrlParams) {
        $UrlParams.Add('count', $true)
        $UrlParams.Add('offset', 0)
    } else {
        $UrlParams = @{
            count = $true
            offset = 0
        }
    }

    # Pagination loop
    do {
        # Combine parameters into query string
        if ($Method -eq "GET") {
            $queryString = ($UrlParams.GetEnumerator() | ForEach-Object {
                [string]::Format("{0}={1}", $_.Key, [uri]::EscapeUriString($_.Value))
            }) -join "&"
            $PagedUri = "{0}?{1}" -f $Url, $queryString
        } else {
            $PagedUri = $Url
        }

        $DecodedPagedUri = [System.Web.HttpUtility]::UrlDecode($PagedUri)

        # Log the request
        if ($i -eq 0) {
            Write-Verbose "Sending $Method request to $DecodedPagedUri"
            $Headers.GetEnumerator() | Where-Object name -ne "Authorization" | ForEach-Object {
                Write-Debug ("{0}: {1}" -f $_.Name, $_.Value)
            }
        } elseif ($i -eq 1) {
            Write-Debug "More records are available for this query."
            Write-Debug "Querying $DecodedPagedUri"
        } elseif ($i -gt 1) {
            Write-Debug "Querying $DecodedPagedUri"
        }

        # Try to fetch the page
        while ($SendCall) {
            try {
                $Splat = @{
                    Uri             = $PagedUri
                    UseBasicParsing = $true
                    Headers         = $Headers
                    Method          = $Method
                    ContentType     = $ContentType
                }
                if($Body) { $Splat.Add('Body', $Body) }
                $response = Invoke-WebRequest @Splat -Verbose:$false -Debug:$false

                # Parse and collect results
                $Results = $Response.Content | ConvertFrom-Json
                $AllResults += $Results

                # Get the total count from the header
                if (-not $TotalCount) {
                    # If total count header exists
                    try {
                        $TotalCount = [Int]$Response.Headers.'X-Total-Count'[0]
                    }
                    catch {
                        if ($_.Exception.Message -eq 'Cannot index into a null array.') {
                            $TotalCount = $Results.Count
                        }
                        else {
                            Write-Output "Something went wrong calculating the total number of objects" | Out-Null
                            throw $_
                        }
                    }
                }

                # Update the offset and remaining limit
                $Offset += ($Results | Measure-Object).Count

                # Add pagination parameters to the UrlParams
                $UrlParams.offset = $Offset

                # Exit retry loop if successful
                $i += 1
                $SendCall = $false
            }
            catch {
                Write-Error ("HTTP {0} {1}: {2}" -f ($_.Exception.Response.StatusCode.value__), ($_.Exception.Response.StatusCode.ToString()), (($_.ErrorDetails | ConvertFrom-Json).messages[0]).text)
                if ($_.Exception.Response.StatusCode.value__ -match '^5\d{2}$' -and $RetryCount -lt $MaxRetries) {
                    $RetryCount += 1
                    Write-Verbose "Retry attempt $RetryCount after a $PauseDuration second pause..."
                    Start-Sleep -Seconds $PauseDuration
                }
                else {
                    $SendCall = $false
                    throw "Failed to retrieve data after $RetryCount attempts."
                }
            }
        }

        # Reset for the next page
        $SendCall = $true
        $RetryCount = 0

    } while ($Offset -lt $TotalCount)

    # Return all results
    Write-Output $AllResults
}