plugins/MsDynamicsCRM365/Private/invoke/Invoke-Dynamics.ps1
Function Invoke-Dynamics { [CmdletBinding()] param ( #[Parameter(Mandatory=$true)][String]$Object # The cleverreach object like groups or mailings (first part after the main url) [Parameter(Mandatory=$false)][String]$Service = "data" ,[Parameter(Mandatory=$false)][String]$ContentType = "application/json; charset=utf-8" ,[Parameter(Mandatory=$false)][String]$Path = "" # The path in the url after the object ,[Parameter(Mandatory=$false)][PSCustomObject]$Query = [PSCustomObject]@{} # Query parameters for the url ,[Parameter(Mandatory=$false)][Switch]$Paging = $false # Automatic paging through the result, only needed for a few calls #,[Parameter(Mandatory=$false)][Int]$Pagesize = 0 # Pagesize, if not defined in settings. For reports the max is 5000. ,[Parameter(Mandatory=$false)][ValidateScript({ If ($_ -is [PSCustomObject]) { [PSCustomObject]$_ # } elseif ($_ -is [System.Collections.Specialized.OrderedDictionary]) { # [System.Collections.Specialized.OrderedDictionary]$_ # } # } ElseIf ($_ -is [System.Collections.ArrayList] -or $_ -is [Array]) { # [System.Collections.ArrayList]$_ } })]$Body = [PSCustomObject]@{} # Body to upload, e.g. for POST and PUT requests, will automatically transformed into JSON ) DynamicParam { # All parameters, except Uri and body (needed as an object) $p = Get-BaseParameters "Invoke-WebRequest" [void]$p.remove("Uri") [void]$p.remove("Body") [void]$p.remove("ContentType") $p } Begin { # check type of body, if present <# If ($Body -is [PSCustomObject]) { Write-Host "PSCustomObject" } ElseIf ($Body -is [System.Collections.ArrayList]) { Write-Host "ArrayList" } else { Throw 'Body datatype not valid' } #> # check url, if it ends with a slash If ( $Script:settings.base.endswith("/") -eq $true ) { $base = $Script:settings.base } else { $base = "$( $Script:settings.base )/" } # Build custom Dynamics365 domain $base = "$( $base )api/$( $Service )/v$( $Script:settings.apiversion )" # Reduce input parameters to only allowed ones $updatedParameters = Skip-UnallowedBaseParameters -Base "Invoke-WebRequest" -Parameters $PSBoundParameters # Output parameters in debug mode If ( $Script:debugMode -eq $true -or $Verbose -eq $true ) { Write-Host "INPUT: $( Convertto-json -InputObject $PSBoundParameters -Depth 99 )" } # Prepare Authentication If ( $Script:settings.token.tokenUsage -eq "consume" ) { $rawToken = ( Get-Content -Path $Script:settings.token.tokenFilePath -Encoding UTF8 -Raw ).replace("`n","").replace("`r","") If ( $Script:settings.token.encryptTokenFile -eq $true ) { $token = Convert-SecureToPlaintext -String $rawToken } else { $token = $rawToken } } elseif ( $Script:settings.token.tokenUsage -eq "generate" ) { $token = Convert-SecureToPlaintext -String $Script:settings.login.accesstoken } else { throw "No token available!" exit 0 # TODO check, if this token is needed or should be another exit code } # Build up header $header = [Hashtable]@{ "Authorization" = "Bearer $( $token )" "OData-MaxVersion" = "4.0" "OData-Version" = "4.0" "If-None-Match" = "null" # according to this: https://bengribaudo.com/blog/2021/04/09/5577/dataverse-web-api-tip-the-always-include-headers #"Prefer" = 'odata.include-annotations="*"' #"Accept" = "application/json" } # Empty the token variables $token = "" $rawToken = "" # Add auth header or just set it If ( $updatedParameters.ContainsKey("Headers") -eq $true ) { $header.Keys | ForEach-Object { $key = $_ $updatedParameters.Headers.Add( $key, $header.$key ) } } else { $updatedParameters.add("Headers",$header) } # Add additional headers from the settings, e.g. for api gateways or proxies $Script:settings.additionalHeaders.PSObject.Properties | ForEach-Object { $updatedParameters.Headers.add($_.Name, $_.Value) } # Set content type, if not present yet If ( $updatedParameters.ContainsKey("ContentType") -eq $false) { $updatedParameters.add("ContentType",$ContentType) } # normalize the path, remove leading and trailing slashes If ( $Path -ne "") { If ( $Path.StartsWith("/") -eq $true ) { $Path = $Path.Substring(1) } If ( $Path.EndsWith("/") -eq $true ) { $Path = $Path -replace ".$" } } } Process { #----------------------------------------------- # PREPARE QUERY #----------------------------------------------- $nvCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) $Query.PSObject.Properties | ForEach-Object { $nvCollection.Add( $_.Name, $_.Value ) } #----------------------------------------------- # PREPARE URL #----------------------------------------------- $uriRequest = [System.UriBuilder]::new("$( $base )/$( $Path )") $uriRequest.Query = $nvCollection.ToString() $updatedParameters.Uri = $uriRequest.Uri.OriginalString #----------------------------------------------- # PREPARE BODY #----------------------------------------------- If ( $updatedParameters.ContainsKey("Body") -eq $true ) { $bodyJson = ConvertTo-Json -InputObject $Body -Depth 99 $updatedParameters.Body = $bodyJson } #----------------------------------------------- # DO THE REQUEST #----------------------------------------------- $finished = $false $continueAfterTokenRefresh = $false $res = [System.Collections.ArrayList]@() Do { # Execute the request try { # Output parameters in debug mode If ( $Script:debugMode -eq $true -or $Verbose -eq $true ) { Write-Host "REST: $( Convertto-json -InputObject $updatedParameters -Depth 99 )" } Write-Verbose -Message "$( $updatedParameters.Method.ToString().ToUpper() ) $( $updatedParameters.Uri )" -verbose If ( $Script:logAPIrequests -eq $true ) { Write-Log -Message "$( $updatedParameters.Method.ToString().ToUpper() ) $( $updatedParameters.Uri )" -severity verbose } $Script:pluginDebug = $updatedParameters #Write-Host ( convertto-json $updatedParameters ) $wrInput = [Hashtable]@{ "Params" = $updatedParameters "RetryHttpErrorList" = $Script:settings.errorhandling.RepeatOnHttpErrors "MaxTriesSpecific" = $Script:settings.errorhandling.MaximumRetriesOnHttpErrorList "MaxTriesGeneric" = $Script:settings.errorhandling.MaximumRetriesGeneric "MillisecondsDelay" = $Script:settings.errorhandling.HttpErrorDelay } $wr = @( Invoke-WebRequestWithErrorHandling @wrInput ) # Parse the result If ( $wr.Content -eq $null ) { $wrContent = [Array]@() } else { # TODO check with utf8 in returned header If ( $wr.headers.'Content-Type' -like "application/json*" ) { $wrContent = convertfrom-json -InputObject $wr.content #-Depth 99 } else { $wrContent = $wr.content } } #$wr = Invoke-WebRequest @updatedParameters -UseBasicParsing } catch { $e = $_ Write-Log -Message $e.Exception.Message -Severity ERROR # parse the response code and body $errResponse = $e.Exception.Response $errBody = Import-ErrorForResponseBody -Err $e # Do this only once if ( $errResponse.StatusCode.value__ -eq 401 -and $continueAfterTokenRefresh -eq $false) { Write-Log -Severity WARNING -Message "401 Unauthorized" try { $newToken = Save-NewToken Write-Log -Severity WARNING -Message "Successful token refresh" $wrInput.Params.Headers.Authorization = "Bearer $( $newToken )" $continueAfterTokenRefresh = $true } catch { Write-Log -Severity ERROR -Message "Token refresh not successful" } If ( $continueAfterTokenRefresh -eq $true ) { Continue } } # $responseStream = $_.Exception.Response.GetResponseStream() # $responseReader = [System.IO.StreamReader]::new($responseStream) # $responseBody = $responseReader.ReadToEnd() # Write-Log -Message $responseBody -Severity ERROR throw $_.Exception } # Choose next page link add results to the collection If ( $Paging -eq $true ) { # If the result has a link to the next page, just follow it If ( $wrContent.psobject.properties.name -contains "@odata.nextLink" ) { Write-Verbose "Next url: $( $wrContent."@odata.nextLink" )" -verbose $updatedParameters.Uri = $wrContent."@odata.nextLink" } else { # If this is less than the page size -> done! $finished = $true } # Add result to return collection [void]$res.AddRange($wrContent.value) } else { # If this is only one request without paging -> done! $finished = $true } # If ( $Verbose -eq $true ) { # Write-log $wr.Headers."Sforce-Limit-Info" -severity verbose #api-usage=2/15000 # } $Script:pluginDebug = $wr.headers } Until ( $finished -eq $true ) #----------------------------------------------- # SAVE CURRENT RATE #----------------------------------------------- # There is also: x-ms-dop-hint and x-ms-ratelimit-time-remaining-xrm-requests # Explained here: https://github.com/MicrosoftDocs/powerapps-docs/blob/main/powerapps-docs/developer/data-platform/api-limits.md If ( $Script:variableCache.Keys -contains "api_rate_remaining" ) { #$Script:variableCache.api_rate_limit = $wr.Headers."X-HubSpot-RateLimit-Daily" $Script:variableCache.api_rate_remaining = $wr.Headers."x-ms-ratelimit-burst-remaining-xrm-requests" } else { #$Script:variableCache.Add("api_rate_limit",$wr.Headers."X-HubSpot-RateLimit-Daily") $Script:variableCache.Add("api_rate_remaining", $wr.Headers."x-ms-ratelimit-burst-remaining-xrm-requests") } #----------------------------------------------- # RETURN #----------------------------------------------- # Always return the deltalink, if it is present If ( $Paging -eq $true ) { If ( $wrContent.psobject.properties.name -contains "@odata.deltaLink" ) { [PSCustomObject]@{ "@odata.deltaLink" = $wrContent."@odata.deltaLink" "value" = $res } } else { [PSCustomObject]@{ "value" = $res } } } else { $wrContent } } End { <# If ( $Paging -eq $true ) { $res } else { $wr } #> } } |