internal/functions/invoke-adoapirequest.ps1


<#
    .SYNOPSIS
        A universal function to interact with Azure DevOps REST API endpoints.
         
    .DESCRIPTION
        The `Invoke-ADOApiRequest` function allows interaction with any Azure DevOps REST API endpoint.
        It requires the organization, a valid authentication token, and the specific API URI.
        The project is optional and will only be included in the URL if specified.
         
    .PARAMETER Organization
        The name of the Azure DevOps organization.
         
    .PARAMETER Project
        The name of the Azure DevOps project (optional).
         
    .PARAMETER Token
        The authentication token for accessing Azure DevOps.
         
    .PARAMETER ApiUri
        The specific Azure DevOps REST API URI to interact with (relative to the organization or project URL).
         
    .PARAMETER Method
        The HTTP method to use for the request (e.g., GET, POST, PUT, DELETE). Default is "GET".
         
    .PARAMETER Body
        The body content to include in the HTTP request (for POST/PUT requests).
         
    .PARAMETER Headers
        Additional headers to include in the request.
         
    .PARAMETER ApiVersion
        The version of the Azure DevOps REST API to use. Default is "7.1".
         
    .PARAMETER OnlyFirstPage
        If enabled, the function will return only the first page of results and stop if a continuation token is present.
         
    .EXAMPLE
        Invoke-ADOApiRequest -Organization "my-org" -ApiUri "_apis/testplan/Plans/123/suites" -Token "my-token"
         
        This example retrieves test suites from the test plan with ID 123 in the specified organization.
         
    .EXAMPLE
        Invoke-ADOApiRequest -Organization "my-org" -ApiUri "_apis/projects" -Token "my-token" -OnlyFirstPage
         
        This example retrieves only the first page of projects in the specified organization.
         
    .NOTES
            - The function uses the Azure DevOps REST API.
            - An authentication token is required.
            - Handles pagination through continuation tokens.
        Author: Oleksandr Nikolaiev
#>


function Invoke-ADOApiRequest {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Organization,

        [Parameter()]
        [string]$Project = $null,

        [Parameter(Mandatory = $true)]
        [string]$Token,

        [Parameter(Mandatory = $true)]
        [string]$ApiUri,

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

        [Parameter()]
        [string]$Body = $null,

        [Parameter()]
        [Hashtable]$Headers = @{}, 

        [Parameter()]
        [string]$ApiVersion = $Script:ADOApiVersion,

        [Parameter()]
        [switch]$OnlyFirstPage
    )
    begin {
        Invoke-TimeSignal -Start
        if (-not $Token) {
            Write-PSFMessage -Level Error -Message "Token is required"
            return
        }
        if (-not $ApiUri) {
            Write-PSFMessage -Level Error -Message "ApiUri is required"
            return
        }
        if (-not $Organization) {
            Write-PSFMessage -Level Error -Message "Organization is required"
            return
        }
        if ($Organization.StartsWith("https://dev.azure.com") -eq $false) {
            $Organization = "https://dev.azure.com/$Organization"
        }

        # Prepare Authorization Header
        if ($Token.StartsWith("Bearer")) {
            $authHeader = @{ Authorization = "$Token"}
        } else {
            $authHeader = @{ Authorization = "Bearer $Token" }
        }

        # Merge additional headers
        $headers = $authHeader + $Headers

        $allResults = @()
        $continuationToken = $null
        $requestUrl = ""
    }
    process {       

        try {
            $statusCode = $null

            do {
                # Construct the full URL with API version and continuation token
                $baseUrl = if ($Project) { "$Organization/$Project" } else { $Organization }
                $baseUrl = $baseUrl.TrimEnd('/')
                $ApiUri = $ApiUri.TrimStart('/').TrimEnd('/')                
                $requestUrl = "$baseUrl/$ApiUri"   
                
                if ($continuationToken) {
                    $requestUrl += "&continuationToken=$continuationToken"
                }
                
                $requestUrl = if ($requestUrl.Contains("?")) 
                { [System.Uri]::EscapeUriString("$requestUrl" + "&api-version=$ApiVersion") } 
                else 
                { [System.Uri]::EscapeUriString("$requestUrl" + "?api-version=$ApiVersion") }

                Write-PSFMessage -Level Verbose -Message "Request URL: $Method $requestUrl"

                if ($PSVersionTable.PSVersion.Major -ge 7) {
                    $response = Invoke-RestMethod -Uri $requestUrl -Headers $headers -Method $Method.ToLower() -Body $Body -ResponseHeadersVariable responseHeaders -StatusCodeVariable statusCode
                    $continuationToken = $responseHeaders['x-ms-continuationtoken']
                } else {
                    $response = Invoke-WebRequest -Uri $requestUrl -Headers $headers -Method $Method.ToLower() -Body $Body -UseBasicParsing
                    $continuationToken = $response.Headers['x-ms-continuationtoken']
                    $statusCode = $response.StatusCode
                    $response = $response.Content | ConvertFrom-Json
                }
                
                if ($statusCode -in @(200, 201, 202, 204 )) {
                    if ($response.value) {
                        $allResults += $response.value
                    } else {
                        $allResults += $response
                    }

                    # If OnlyFirstPage is enabled, stop after the first response
                    if ($OnlyFirstPage -and $continuationToken) {
                        break
                    }
                } else {
                    Write-PSFMessage -Level Error -Message "The request failed with status code: $($statusCode)"
                }

            } while ($continuationToken)

            return @{
                Results = $allResults
                Count = $allResults.Count
            }
        } catch {
            Write-PSFMessage -Level Error -Message "Something went wrong during request to ADO: $($_.ErrorDetails.Message)" -Exception $PSItem.Exception
            Stop-PSFFunction -Message "Stopping because of errors"
            return
        }
    }
    end {
        Invoke-TimeSignal -End        
    }
}