Private/Invoke-FleetDMRequest.ps1

function Invoke-FleetDMRequest {
    <#
    .SYNOPSIS
        Internal function to make API requests to FleetDM
     
    .DESCRIPTION
        This is the core function that handles all API communication with FleetDM.
        It manages authentication, error handling, pagination, and response processing.
     
    .PARAMETER Endpoint
        The API endpoint path (without the base URI or /api/v1/fleet/ prefix)
     
    .PARAMETER Method
        The HTTP method to use (GET, POST, PUT, PATCH, DELETE)
     
    .PARAMETER Body
        Hashtable containing the request body data (will be converted to JSON)
     
    .PARAMETER QueryParameters
        Hashtable of query parameters to append to the URI
     
    .PARAMETER FollowPagination
        If specified, automatically follows pagination to retrieve all results
     
    .PARAMETER Raw
        If specified, returns the raw response without processing
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Endpoint,
        
        [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')]
        [string]$Method = 'GET',
        
        [hashtable]$Body,
        
        [hashtable]$QueryParameters,
        
        [switch]$FollowPagination,
        
        [switch]$Raw
    )
    
    begin {
        # Check if connected
        if (-not $script:FleetDMConnection) {
            throw "Not connected to FleetDM. Please run Connect-FleetDM first."
        }
        
        # Remove leading slash from endpoint if present
        $Endpoint = $Endpoint.TrimStart('/')
        
        # Build the full URI
        $uri = "$($script:FleetDMConnection.BaseUri)/api/v1/fleet/$Endpoint"
        
        Write-Verbose "Preparing $Method request to: $uri"
    }
    
    process {
        Write-Verbose "Using headers: $($script:FleetDMConnection.Headers | ConvertTo-Json -Compress)"
        # Add query parameters if provided
        if ($QueryParameters -and $QueryParameters.Count -gt 0) {
            $queryString = @()
            
            foreach ($param in $QueryParameters.GetEnumerator()) {
                $encodedKey = [System.Web.HttpUtility]::UrlEncode($param.Key)
                $encodedValue = [System.Web.HttpUtility]::UrlEncode($param.Value.ToString())
                $queryString += "$encodedKey=$encodedValue"
            }
            
            $separator = if ($uri.Contains('?')) { '&' } else { '?' }
            $uri = "$uri$separator$($queryString -join '&')"
            
            Write-Verbose "Full URI with parameters: $uri"
        }
        Write-Verbose "HTTP Method: $Method"
        # Prepare the request parameters
        $requestParams = @{
            Uri = $uri
            Method = $Method
            Headers = $script:FleetDMConnection.Headers
            ContentType = 'application/json'
            WebSession = $script:FleetDMWebSession
            ErrorAction = 'Stop'
        }
        Write-Verbose "Request parameters: $($requestParams | ConvertTo-Json -Compress)"
        # Add body if provided
        if ($Body -and $Body.Count -gt 0) {
            $jsonBody = $Body | ConvertTo-Json -Depth 10 -Compress
            $requestParams['Body'] = $jsonBody
            Write-Verbose "Request body: $jsonBody"
        }
        Write-Verbose "Sending request to FleetDM API"
        try {
            if ($FollowPagination -and $Method -eq 'GET') {
                Write-Verbose "Pagination enabled. Fetching all results."
                # Handle pagination
                $allResults = @()
                $page = 0
                $hasMore = $true
                
                while ($hasMore) {
                    # Add page parameter
                    $currentUri = if ($uri -match '\?') { "$uri&page=$page" } else { "$uri?page=$page" }
                    $requestParams['Uri'] = $currentUri
                    
                    Write-Verbose "Fetching page $page"
                    
                    $response = Invoke-RestMethod @requestParams
                    
                    # Add results to collection
                    if ($response -is [System.Management.Automation.PSCustomObject]) {
                        # Response is an object with data and metadata
                        if ($response.PSObject.Properties.Name -contains 'hosts') {
                            $allResults += $response.hosts
                        }
                        elseif ($response.PSObject.Properties.Name -contains 'policies') {
                            $allResults += $response.policies
                        }
                        elseif ($response.PSObject.Properties.Name -contains 'queries') {
                            $allResults += $response.queries
                        }
                        elseif ($response.PSObject.Properties.Name -contains 'software') {
                            $allResults += $response.software
                        }
                        elseif ($response.PSObject.Properties.Name -contains 'users') {
                            $allResults += $response.users
                        }
                        elseif ($response.PSObject.Properties.Name -contains 'teams') {
                            $allResults += $response.teams
                        }
                        else {
                            # If no recognized collection property, add the whole response
                            $allResults += $response
                        }
                        
                        # Check for more pages
                        if ($response.meta -and $response.meta.has_next_results) {
                            $page++
                        }
                        else {
                            $hasMore = $false
                        }
                    }
                    else {
                        # Response is likely an array or simple object
                        $allResults += $response
                        $hasMore = $false
                    }
                }
                
                Write-Verbose "Retrieved $($allResults.Count) total items across $($page + 1) pages"
                
                if ($Raw) {
                    return $allResults
                }
                else {
                    return $allResults
                }
            }
            else {
                Write-Verbose "No pagination requested. Sending single request."
                # Single request without pagination
                $response = Invoke-RestMethod @requestParams
                
                Write-Verbose "Request completed successfully"
                
                if ($Raw) {
                    return $response
                }
                else {
                    return $response
                }
            }
        }
        catch {
            # Handle errors
            Handle-FleetDMError -ErrorRecord $_
        }
    }
}

function Handle-FleetDMError {
    <#
    .SYNOPSIS
        Internal function to handle FleetDM API errors
     
    .DESCRIPTION
        Processes error responses from the FleetDM API and throws appropriate exceptions
     
    .PARAMETER ErrorRecord
        The error record from the failed API call
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )
    
    $exception = $ErrorRecord.Exception
    $response = $exception.Response
    
    if ($response) {
        $statusCode = [int]$response.StatusCode
        $statusDescription = $response.StatusDescription
        
        # Try to parse the error response body
        $errorMessage = $statusDescription
        $errorDetails = $null
        
        try {
            $stream = $response.GetResponseStream()
            $reader = New-Object System.IO.StreamReader($stream)
            $reader.BaseStream.Position = 0
            $responseBody = $reader.ReadToEnd()
            $reader.Close()
            
            if ($responseBody) {
                $errorContent = $responseBody | ConvertFrom-Json
                
                if ($errorContent.message) {
                    $errorMessage = $errorContent.message
                }
                
                if ($errorContent.errors) {
                    $errorDetails = $errorContent.errors
                }
                
                if ($errorContent.uuid) {
                    Write-Verbose "Error UUID: $($errorContent.uuid)"
                }
            }
        }
        catch {
            Write-Verbose "Could not parse error response body"
        }
        
        # Create error message based on status code
        switch ($statusCode) {
            400 {
                # Check for specific error reasons in the details
                $isPremiumError = $false
                if ($errorDetails) {
                    foreach ($detail in $errorDetails) {
                        if ($detail.reason -like "*premium license*") {
                            $isPremiumError = $true
                            break
                        }
                    }
                }
                
                if ($isPremiumError) {
                    throw "This feature requires a Fleet Premium license"
                }
                else {
                    $fullMessage = "Bad Request: $errorMessage"
                    if ($errorDetails) {
                        $fullMessage += "`nDetails: $($errorDetails | ConvertTo-Json -Compress)"
                    }
                    throw $fullMessage
                }
            }
            401 {
                throw "Authentication failed. Your session may have expired. Please run Connect-FleetDM again."
            }
            403 {
                throw "Access denied. You don't have permission to perform this operation. Error: $errorMessage"
            }
            404 {
                throw "Resource not found. The requested item does not exist. Error: $errorMessage"
            }
            408 {
                throw "Request timeout. The operation took too long to complete. Error: $errorMessage"
            }
            429 {
                # Rate limiting
                $retryAfter = $null
                
                if ($errorMessage -match 'retry after:\s*(\d+)s') {
                    $retryAfter = $matches[1]
                }
                
                if ($retryAfter) {
                    Write-Warning "Rate limit exceeded. Please wait $retryAfter seconds before retrying."
                    throw "Rate limit exceeded. Retry after $retryAfter seconds."
                }
                else {
                    throw "Rate limit exceeded. Please wait before retrying."
                }
            }
            500 {
                throw "Internal server error. FleetDM encountered an error. Error: $errorMessage"
            }
            502 {
                throw "Bad gateway. FleetDM service may be temporarily unavailable."
            }
            503 {
                throw "Service unavailable. FleetDM service is temporarily unavailable."
            }
            default {
                throw "FleetDM API error (HTTP $statusCode): $errorMessage"
            }
        }
    }
    else {
        # Non-HTTP error (network, timeout, etc.)
        throw "Failed to communicate with FleetDM: $($exception.Message)"
    }
}