Private/Invoke-FreshworksRestMethod.ps1

function Invoke-FreshworksRestMethod {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    <#
        .SYNOPSIS
            Invoke a REST API call on Freshworks.
 
        .DESCRIPTION
            Invoke a REST API call on Freshworks. This is wrapper function for Invoke-WebRequest to
            define the authorization and perform other requirements (e.g. TLS) in a single function
            versus definition in each function.
 
        .PARAMETER AuthorizationToken
            The encoded Authorization Token provided by Freshworks.
 
        .PARAMETER Url
            The REST API URL that is being called.
 
        .PARAMETER Method
            The HTTP Method that is being used in the API call e.g. (HEAD, GET, POST, PATCH, or DELETE).
 
        .PARAMETER Body
            SOAP, JSON, XML or formatted body for passed in the API call.
 
        .PARAMETER ContentType
            Specified ContentType of the Body that is being passed (e.g. application\json).
 
        .EXAMPLE
            $params = @{
                Uri = $uri
                Method = 'GET'
                ErrorAction = 'Stop'
            }
 
            $result = Invoke-FreshworksRestMethod @params
 
            Performs a GET on a function defined URI.
 
        .NOTES
            This is where the Invoke-RestMethod is called for all FreshservicePS cmdlets. This is the only
            cmdlet that requires authentication data such as AuthorizationToken and settings
            to manipulate certificate security and TLS. This is a internal private function
    #>


        param (
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'Api Token for authenication with Freshworks REST API',
                       ParameterSetName = 'default',
                       Position = 0
            )]
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'Api Token for authenication with Freshworks REST API',
                       ParameterSetName = 'Form',
                       Position = 0
            )]
            [string]$AuthorizationToken = $MyInvocation.MyCommand.Module.PrivateData['FreshserviceApiToken'],
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'REST API Headers',
                       ParameterSetName = 'default',
                       Position = 1
            )]
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'REST API Headers',
                       ParameterSetName = 'Form',
                       Position = 1
            )]
            [hashtable]$Headers,
            [Parameter(Mandatory = $True,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'REST API Uri that is being requested',
                       ParameterSetName = 'default',
                       Position = 2
            )]
            [Parameter(Mandatory = $True,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'REST API Uri that is being requested',
                       ParameterSetName = 'Form',
                       Position = 2
            )]
            [string]$Uri,
            [Parameter(Mandatory = $True,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'REST Method being called',
                       ParameterSetName = 'default',
                       Position = 3
            )]
            [Parameter(Mandatory = $True,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'REST Method being called',
                       ParameterSetName = 'Form',
                       Position = 3
            )]
            [ValidateSet('DELETE', 'GET', 'PUT', 'POST','PATCH')]
            [string]$Method,
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'Message body',
                       ParameterSetName = 'default',
                       Position = 4
            )]
            $Body,
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'ContentType of passed Body for REST header',
                       ParameterSetName = 'default',
                       Position = 5
            )]
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'ContentType of passed Body for REST header',
                       ParameterSetName = 'Form',
                       Position = 5
            )]
            [string]$ContentType = 'application/json',
            [Parameter(Mandatory = $False,
                       ValueFromPipelineByPropertyName = $True,
                       HelpMessage = 'Converts a dictionary to a multipart/form-data submission. Form may not be used with Body. If ContentType is used, it is ignored.',
                       ParameterSetName = 'Form',
                       Position = 6
            )]
            [Collections.IDictionary]$Form

        )
        begin {
            $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
            $PrivateData  = $MyInvocation.MyCommand.Module.PrivateData

            if ( $MyInvocation.MyCommand.Module.PrivateData['FreshserviceApiToken'] ) {
                Write-Verbose 'Appending Authorization header'
                if ($Headers) {
                    $Headers.Add( "Authorization", ("Basic {0}" -f $AuthorizationToken) )
                }
                else {
                    $Headers = @{"Authorization" = ("Basic {0}" -f $AuthorizationToken)}
                }
            }
            else {
                Write-Warning -Message ('Connection settings must be set with Set-FreshWorksConnectionSettings before making Freshworks API calls')
                break
            }

        } #begin
        process {
            Write-Verbose -Message ('{0} - Initiating REST API call to {1} with API key: {2}' -f $MyInvocation.MyCommand.Name, $Uri, $AuthorizationToken)

            $restParams = @{
                Uri         = $Uri
                Method      = $Method
                Headers     = $Headers
                ErrorAction = 'Stop'
            }

            if ( $Body ) {
                $restParams.Add( 'Body', $Body )
            }

            if ($Form) {
                $restParams.Add('ContentType', 'multipart/form-data')
                $restParams.Add( 'Form', $Form )
            }
            else {
                $restParams.Add('ContentType', $ContentType)
            }

            Write-Verbose -Message ('{0} - REST call parameters:' -f $MyInvocation.MyCommand.Name)
            foreach ($param in $restParams.GetEnumerator()) {
                if ($param.Key -eq 'Headers') {
                    Write-Verbose -Message ('{0} - Headers:' -f $MyInvocation.MyCommand.Name)
                    foreach ($header in $param.Value.GetEnumerator()) {
                        Write-Verbose -Message ('{2} - {0}: {1}' -f $header.Key, $header.Value, $MyInvocation.MyCommand.Name)
                    }
                }
                else {
                    #Skip the Body because we handled obfusfication above
                    Write-Verbose -Message ('{2} - {0}: {1}' -f $param.Key, $param.Value, $MyInvocation.MyCommand.Name)
                }
            }

            Write-Verbose -Message ('{0} - Invoking REST {1} Method on {2}...' -f $MyInvocation.MyCommand.Name, $Method, $apiUri)
            try {
                # Force TLS 1.2 protocol. Invoke-RestMethod uses 1.0 by default
                Write-Verbose -Message ('{0} - Forcing TLS 1.2 protocol for invoking REST method.' -f $MyInvocation.MyCommand.Name)
                [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

                $results = Invoke-WebRequest @restParams
                Write-Verbose -Message ('Returned status {0} with code {1}.' -f $results.StatusDescription, $results.StatusCode)
                $rateTotal = $results.Headers['X-Ratelimit-Total'][0]
                $rateRemaining = $results.Headers['X-Ratelimit-Remaining'][0]

                If ($rateTotal -gt 0 -and $rateRemaining -gt 0) {
                    $pctRateUsed = [math]::Round(($rateTotal-$rateRemaining)/$rateTotal * 100, 2)
                    Write-Verbose -Message ('Current FreshService minute rate limit is {0} with {1} calls remaining ({2}% used) .' -f $rateTotal, $rateRemaining,$pctRateUsed)

                    if ($PrivateData.FreshserviceThrottling -eq $true) {
                        # The API rate limit is applied on an account wide basis irrespective of factors such as
                        # the number of agents or IP addresses used to make the calls. There are numerous API calls that can consume multiple API calls
                        # for single get operations (e.g. Get-FSAsset -IncludeTypeFields = 3 API credits for each call). Throttling will slow
                        # the API calls down ggadually the closer the query gets to consuming all calls forcing a 429 Retry-After which affects all API
                        # calls to the account.

                        if ($pctRateUsed -ge 70.00) {
                            switch ($pctRateUsed) {
                                {$PSItem -ge 70.00} {$sleepInSecs = 5}
                                {$PSItem -ge 80.00} {$sleepInSecs = 15}
                                {$PSItem -ge 90.00} {$sleepInSecs = 30}
                            }

                            Write-Warning -Message ('Executing {0}. FreshService API minute rate limit above 70% threshold with {1} total calls available and {2} calls remaining ({3}% used). Artificially slowing calls by {4} second. See Connect-Freshservice for options.' -f $uri, $rateTotal, $rateRemaining,$pctRateUsed,$sleepInSecs)
                            Start-Sleep -Seconds $sleepInSecs
                        }
                    }
                }

            }
            catch {
                $ex = $_
                Write-Verbose -Message ("Catching exception {0} with status code {1}" -f $ex.Exception.GetType().FullName, $ex.Exception.Response.value__)
                switch ($ex.Exception.Response.StatusCode.value__) {
                    '429' {
                        [int]$sleepInSecs = $ex.Exception.Response.Headers.GetValues('Retry-After')[0]
                        Write-Warning -Message ('API rate limit reached. Automatically sleeping for {0} seconds.' -f $sleepInSecs)
                        Write-Verbose -Message ('API rate limit reached. Automatically sleeping for {0} seconds.' -f $sleepInSecs)
                        Start-Sleep -Seconds $sleepInSecs
                        # Create object schema to resend the header link to re-run last API call that terminated with 429
                        $results = [PSCustomObject]@{
                            Content = $null
                            Headers = @{
                                Link = '<{0}>' -f $uri
                            }
                        }

                    }
                    default {
                        Write-Verbose -Message ("Throwing Default exception of type {0}" -f $ex.Exception.GetType().FullName)
                        Throw $ex
                    }
                }

            }
        } #process
        end {
            Write-Verbose -Message ('{0} - Completed REST {1} Method on {2} in {3:c}.' -f $MyInvocation.MyCommand.Name, $Method, $apiUri, $stopwatch.Elapsed)
            $results
        } #end
    } #Invoke-FreshworksRestMethod