Private/Api/Invoke-ApiMethod.ps1
|
<#
Copyright (c) 2025-2026 Robert Faddes SPDX-License-Identifier: MPL-2.0 #> <# .SYNOPSIS Invokes an API method, building the request parameters and handling authentication, and pagination as needed. .DESCRIPTION This function constructs the necessary parameters for an API request, including authentication headers and query parameters. It checks for token expiration and refreshes the token if auto-refresh is enabled. It also supports paginated requests by automatically fetching subsequent pages until all data is retrieved. #> function Invoke-ApiMethod { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory)] [string] $Path, [Microsoft.PowerShell.Commands.WebRequestMethod] $Method = 'Get', [hashtable] $Parameters, [object] $Body, # Enable pagination [Parameter( ParameterSetName = 'Paginate', Mandatory = $false )] [switch] $Paginate, # Name of the element in the response that contains the paginated items [Parameter( ParameterSetName = 'Paginate', Mandatory = $true )] [string] $PageElement ) # Ensure we are connected and have a valid token before making the API call if (-not $script:RMMAuth) { throw "Not connected. Use Connect-DattoRMM first." } # Check token expiration with buffer (UTC comparison) # Refresh proactively before expiry to prevent mid-pagination failures $Now = [datetime]::UtcNow $RefreshThreshold = $script:RMMAuth.ExpiresAt.AddMinutes(-$Script:TokenRefreshBufferMinutes) if ($Now -gt $RefreshThreshold) { if ($script:RMMAuth.AutoRefresh) { # Refresh $RefreshConnectParams = @{ Key = $script:RMMAuth.Key Secret = $script:RMMAuth.Secret AutoRefresh = $true } switch ($Script:RMMAuth.Keys) { 'Proxy' {$RefreshConnectParams.Proxy = $script:RMMAuth.Proxy} 'ProxyCredential' {$RefreshConnectParams.ProxyCredential = $script:RMMAuth.ProxyCredential} } Connect-DattoRMM @RefreshConnectParams } else { throw "Token expired. Reconnect with Connect-DattoRMM. Use -AutoRefresh to enable automatic token refresh." } } # Trim '/' and '?' from path if present to avoid double slashes in URI and issues with query parameters if ($Path.StartsWith('/')) { $Path = $Path.TrimStart('/') } if ($Path.EndsWith('?')) { $Path = $Path.TrimEnd('?') } # Build the request parameters for Invoke-RestMethod, including authentication headers and any query parameters $RequestParams = @{ Uri = "$API/$Path" Method = $Method ContentType = 'application/json' Headers = $RMMAuth.AuthHeader TimeoutSec = $Script:ApiMethodRetry.TimeoutSeconds } # Add proxy settings if configured switch ($Script:RMMAuth.Keys) { 'Proxy' {$RequestParams.Proxy = $script:RMMAuth.Proxy} 'ProxyCredential' {$RequestParams.ProxyCredential = $script:RMMAuth.ProxyCredential} } if ($Parameters) { $QueryParams = @($Parameters.GetEnumerator() | ForEach-Object {"$($_.Key)=$($_.Value)"}) $RequestParams.Uri += '?' + ($QueryParams -join '&') } # Add page size parameter for paginated requests if ($Paginate -and $Script:PageSize) { $PageSizeParam = "max=$($Script:PageSize)" if ($RequestParams.Uri -match '\?') { $RequestParams.Uri += "&$PageSizeParam" } else { $RequestParams.Uri += "?$PageSizeParam" } } if ($Body) { $RequestParams.Body = $Body | ConvertTo-Json -Depth 10 $RequestParams.ContentType = 'application/json' } try { Write-Debug "Invoking RMM API: $Method $Path" Write-Debug "Uri: $($RequestParams.Uri)" # Classify the operation for multi-bucket throttle tracking $OperationName = Resolve-ThrottleOperationName -Path $Path -Method $Method if ($Paginate) { $Result = Invoke-ApiRestMethod -Parameters $RequestParams -Method $Method -OperationName $OperationName # Parse the original URI to extract query parameters (excluding max and page) $OriginalUri = [System.Uri]$RequestParams.Uri $OriginalParams = @{} if ($OriginalUri.Query) { $QueryString = $OriginalUri.Query.TrimStart('?') foreach ($Param in $QueryString.Split('&')) { $KeyValue = $Param.Split('=') if ($KeyValue.Count -eq 2 -and $KeyValue[0] -notin @('max', 'page')) { $OriginalParams[$KeyValue[0]] = $KeyValue[1] } } } $Result.$PageElement while ($Result.pageDetails.nextPageUrl) { $NextUrl = $Result.pageDetails.nextPageUrl # If we have original parameters, check which ones are missing from nextPageUrl - workaround for API not preserving all query params if ($OriginalParams.Count -gt 0) { $NextUri = [System.Uri]$NextUrl $ExistingParams = @() # Parse the nextPageUrl to see what parameters it already has if ($NextUri.Query) { $NextQueryString = $NextUri.Query.TrimStart('?') foreach ($Param in $NextQueryString.Split('&')) { $KeyValue = $Param.Split('=') if ($KeyValue.Count -eq 2) { $ExistingParams += $KeyValue[0] } } } # Only add parameters that are missing from the nextPageUrl $MissingParams = $OriginalParams.GetEnumerator() | Where-Object { $_.Key -notin $ExistingParams } if ($MissingParams) { $AdditionalParams = ($MissingParams | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join '&' if ($NextUrl -match '\?') { $NextUrl = "$NextUrl&$AdditionalParams" } else { $NextUrl = "$NextUrl?$AdditionalParams" } } } Write-Debug "Fetching next page: $NextUrl" $RequestParams.Uri = $NextUrl $Result = Invoke-ApiRestMethod -Parameters $RequestParams -Method $Method -OperationName $OperationName $Result.$PageElement } } else { Invoke-ApiRestMethod -Parameters $RequestParams -Method $Method -OperationName $OperationName } } catch { throw $_ } } # SIG # Begin signature block # MIIF+wYJKoZIhvcNAQcCoIIF7DCCBegCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC81vS8yFrKIumT # vvJCUTFU5LAn04THfDArsjcl82EOSKCCA04wggNKMIICMqADAgECAhB464iXHfI6 # gksEkDDTyrNsMA0GCSqGSIb3DQEBCwUAMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRk # ZXMxIzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nMB4XDTI2MDMz # MTAwMTMzMFoXDTI4MDMzMTAwMjMzMFowPTEWMBQGA1UECgwNUm9iZXJ0IEZhZGRl # czEjMCEGA1UEAwwaRGF0dG9STU0uQ29yZSBDb2RlIFNpZ25pbmcwggEiMA0GCSqG # SIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn1EpMYQgl1RgWzQj2+wp2mvdfb3UsaBS # nxEVGoQ0gj96tJ2MHAF7zsITdUjwaflKS1vE6wAlOg5EI1V79tJCMxzM0bFpOdR1 # L5F2HE/ovIAKNkHxFUF5qWU8vVeAsOViFQ4yhHpzLen0WLF6vhmc9eH23dLQy5fy # tELZQEc2WbQFa4HMAitP/P9kHAu6CUx5s4woLIOyyR06jkr3l9vk0sxcbCxx7+dF # RrsSLyPYPH+bUAB8+a0hs+6qCeteBuUfLvGzpMhpzKAsY82WZ3Rd9X38i32dYj+y # dYx+nx+UEMDLjDJrZgnVa8as4RojqVLcEns5yb/XTjLxDc58VatdAgMBAAGjRjBE # MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU # H+B0vf97dYXqdUX1YMcWhFsY6fcwDQYJKoZIhvcNAQELBQADggEBAJmD4EEGNmcD # 1JtFoRGxuLJaTHxDwBsjqcRQRE1VPZNGaiwIm8oSQdHVjQg0oIyK7SEb02cs6n6Y # NZbwf7B7WZJ4aKYbcoLug1k1x9SoqwBmfElECeJTKXf6dkRRNmrAodpGCixR4wMH # KXqwqP5F+5j7bdnQPiIVXuMesxc4tktz362ysph1bqKjDQSCBpwi0glEIH7bv5Ms # Ey9Gl3fe+vYC5W06d2LYVebEfm9+7766hsOgpdDVgdtnN+e6uwIJjG/6PTG6TMDP # y+pr5K6LyUVYJYcWWUTZRBqqwBHiLGekPbxrjEVfxUY32Pq4QfLzUH5hhUCAk4HN # XpF9pOzFLMUxggIDMIIB/wIBATBRMD0xFjAUBgNVBAoMDVJvYmVydCBGYWRkZXMx # IzAhBgNVBAMMGkRhdHRvUk1NLkNvcmUgQ29kZSBTaWduaW5nAhB464iXHfI6gksE # kDDTyrNsMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC # gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG # CisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIHgxSd/RFESVsZ3CE8kjUZcgbyqH # fijJDKdX55yb+KNSMA0GCSqGSIb3DQEBAQUABIIBAEK8zpXf85CqiqDrYmBmoYhr # 2Z3GdjwApHf0lTrMQgsvMp0/u+xIEEmypKkTdF/+u3XSlFsdDPDqLC7o8p04xb8N # guXTpwzpgCnjVBbwuu7kwAhScidxwUCndLM12uzj1cM+O5TG+jUxTTWyjg48wxyQ # Srljg2qauEx3QL62RHVjUyzFHF6jIIOuGReaZUcIK2BRc8RQjJJf+L7+GVsQhLEa # F3RjLAbDF6feblG1OW+cA2/3bhdYoAGPEnaMDkwJiffo1R8n7xRjTcyvOp0p27H+ # y8vMMWF6tjcWCjhpw+wzyuzQ7ZyVjK/12FSZR4EN1XZFWJNfjKky78aoZNu1Vlo= # SIG # End signature block |