Private/Invoke-DataverseRequest.ps1
function Invoke-DataverseRequest { <# .SYNOPSIS Internal helper function that handles common request formation and processing for Dataverse API calls. .DESCRIPTION Standardizes request handling for Dataverse API calls, including proper header formation, token validation, error handling, and response processing. .PARAMETER EnvironmentUrl The URL of the Power Platform environment. .PARAMETER RelativeUrl The API endpoint relative to the environment URL. .PARAMETER Token The authentication token object from Get-DataverseAuthToken. .PARAMETER Method The HTTP method to use (GET, POST, PATCH, DELETE). .PARAMETER Body The request body to send (for POST/PATCH requests). .PARAMETER Headers Additional headers to include in the request. .PARAMETER ContentType The content type for the request, defaults to "application/json". .EXAMPLE $response = Invoke-DataverseRequest -EnvironmentUrl "https://myorg.crm.dynamics.com" -RelativeUrl "/api/data/v9.2/accounts" -Token $token -Method "GET" .NOTES This is an internal helper function not exported by the module. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$EnvironmentUrl, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$RelativeUrl, [Parameter(Mandatory = $true)] [ValidateNotNull()] [PSCustomObject]$Token, [Parameter(Mandatory = $true)] [ValidateSet("GET", "POST", "PATCH", "DELETE")] [string]$Method, [Parameter(Mandatory = $false)] [object]$Body = $null, [Parameter(Mandatory = $false)] [hashtable]$Headers = @{}, [Parameter(Mandatory = $false)] [string]$ContentType = "application/json" ) Write-Verbose "Starting Dataverse API request: $Method $RelativeUrl" # Validate token if (-not (Test-DataverseToken -Token $Token)) { throw [PSCustomObject]@{ ErrorCode = "AuthenticationError" Message = "Authentication token is invalid or expired" Details = "Please obtain a new token using Get-DataverseAuthToken" RequestId = "" } } # Normalize environment URL if ($EnvironmentUrl.EndsWith("/")) { $EnvironmentUrl = $EnvironmentUrl.TrimEnd("/") } # Ensure relative URL starts with a slash if (-not $RelativeUrl.StartsWith("/")) { $RelativeUrl = "/$RelativeUrl" } # Construct full URL $fullUrl = "$EnvironmentUrl$RelativeUrl" Write-Verbose "Request URL: $fullUrl" # Prepare request headers $requestHeaders = @{ "Authorization" = "$($Token.TokenType) $($Token.AccessToken)" "Accept" = "application/json" "OData-MaxVersion" = "4.0" "OData-Version" = "4.0" } # Add additional headers foreach ($key in $Headers.Keys) { $requestHeaders[$key] = $Headers[$key] } # Create parameters for Invoke-RestMethod $params = @{ Uri = $fullUrl Method = $Method Headers = $requestHeaders ErrorAction = "Stop" } # Add body for POST/PATCH operations if ($Body -and ($Method -eq "POST" -or $Method -eq "PATCH")) { $params.Add("ContentType", $ContentType) # Convert hashtable/custom object to JSON if content type is application/json if ($ContentType -eq "application/json" -and $Body -isnot [string]) { $params.Add("Body", (ConvertTo-Json -InputObject $Body -Depth 10)) Write-Verbose "Request body: $(ConvertTo-Json -InputObject $Body -Depth 3 -Compress)" } else { $params.Add("Body", $Body) Write-Verbose "Request has body with content type: $ContentType" } } try { # Execute the request $startTime = Get-Date $response = Invoke-RestMethod @params $duration = (Get-Date) - $startTime Write-Verbose "Request completed in $($duration.TotalMilliseconds)ms" return $response } catch { $errorRecord = $_ $statusCode = $null $errorData = $null $responseBody = $null # Extract status code if available if ($errorRecord.Exception.Response) { $statusCode = [int]$errorRecord.Exception.Response.StatusCode # Try to get response body for more error details try { $responseStream = $errorRecord.Exception.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($responseStream) $responseBody = $reader.ReadToEnd() $reader.Close() if ($responseBody) { $errorData = ConvertFrom-Json $responseBody -ErrorAction SilentlyContinue } } catch { Write-Verbose "Could not read error response body: $($_.Exception.Message)" } } # Construct standardized error object $errorDetails = @{ ErrorCode = if ($statusCode) { $statusCode } else { "RequestError" } Message = "Dataverse API request failed" Details = $errorRecord.Exception.Message RequestId = "" } # Add more context from the error response if available if ($errorData) { if ($errorData.error) { $errorDetails.ErrorCode = if ($errorData.error.code) { $errorData.error.code } else { $errorDetails.ErrorCode } $errorDetails.Message = if ($errorData.error.message) { $errorData.error.message } else { $errorDetails.Message } # Some Dataverse errors include a requestid if ($errorData.error.innererror -and $errorData.error.innererror.requestid) { $errorDetails.RequestId = $errorData.error.innererror.requestid } } } Write-Error "Dataverse API Error: [$($errorDetails.ErrorCode)] $($errorDetails.Message)" throw [PSCustomObject]$errorDetails } } |