Private/Invoke-UKGRequest.ps1
|
function Invoke-UKGRequest { <# .SYNOPSIS Central function for making API calls to the UKG HR Service Delivery API. .DESCRIPTION Handles all HTTP requests to the API, including authentication, token refresh, pagination, and error handling. .PARAMETER Endpoint The API endpoint path (without base URL). .PARAMETER Method The HTTP method to use (GET, POST, PUT, PATCH, DELETE, HEAD). .PARAMETER Body The request body as a hashtable (will be converted to JSON). .PARAMETER QueryParameters Query string parameters as a hashtable. .PARAMETER FilePath Path to a file for multipart/form-data uploads. .PARAMETER FileName Name to use for the uploaded file (defaults to file basename). .PARAMETER ContentType Content type for file uploads. .PARAMETER ReturnHeaders If specified, returns both data and headers in a wrapper object. .PARAMETER RawResponse If specified, returns the raw response without parsing. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Endpoint, [Parameter()] [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD')] [string]$Method = 'GET', [Parameter()] [hashtable]$Body, [Parameter()] [hashtable]$QueryParameters, [Parameter()] [string]$FilePath, [Parameter()] [string]$FileName, [Parameter()] [string]$ContentType, [Parameter()] [switch]$ReturnHeaders, [Parameter()] [switch]$RawResponse ) # Verify we have an active session if ($null -eq $Script:UKGSession) { $errorRecord = ConvertTo-UKGError -Response @{ code = 'not_connected'; message = 'Not connected to UKG API. Use Connect-UKG first.' } -StatusCode 0 $PSCmdlet.ThrowTerminatingError($errorRecord) } # Check if token needs refresh (refresh 60 seconds before expiry) $refreshThreshold = (Get-Date).AddSeconds(60) if ($Script:UKGSession.TokenExpiry -lt $refreshThreshold) { Write-Verbose "Token expired or expiring soon, refreshing..." try { Update-UKGToken } catch { $errorRecord = ConvertTo-UKGError -Response @{ code = 'token_refresh_failed'; message = "Failed to refresh token: $_" } -StatusCode 0 -Exception $_.Exception $PSCmdlet.ThrowTerminatingError($errorRecord) } } # Build the full URL $baseUrl = $Script:UKGSession.BaseUrl $url = "$baseUrl$Endpoint" # Add query parameters if ($null -ne $QueryParameters -and $QueryParameters.Count -gt 0) { $queryParts = @() foreach ($key in $QueryParameters.Keys) { $value = $QueryParameters[$key] if ($null -ne $value) { # Handle arrays if ($value -is [array]) { foreach ($v in $value) { $queryParts += "$([System.Uri]::EscapeDataString($key))=$([System.Uri]::EscapeDataString($v.ToString()))" } } else { $queryParts += "$([System.Uri]::EscapeDataString($key))=$([System.Uri]::EscapeDataString($value.ToString()))" } } } if ($queryParts.Count -gt 0) { $url += "?" + ($queryParts -join "&") } } Write-Verbose "Making $Method request to: $url" # Build headers $headers = @{ 'Authorization' = "Bearer $($Script:UKGSession.AccessToken)" 'Accept' = 'application/json' } # Build request parameters $requestParams = @{ Uri = $url Method = $Method Headers = $headers UseBasicParsing = $true ErrorAction = 'Stop' } # Handle file uploads (multipart/form-data) if ($FilePath) { if (-not (Test-Path -Path $FilePath -PathType Leaf)) { $errorRecord = ConvertTo-UKGError -Response @{ code = 'file_not_found'; message = "File not found: $FilePath" } -StatusCode 0 $PSCmdlet.ThrowTerminatingError($errorRecord) } $fileBytes = [System.IO.File]::ReadAllBytes($FilePath) $actualFileName = if ($FileName) { $FileName } else { [System.IO.Path]::GetFileName($FilePath) } # Determine content type $fileContentType = if ($ContentType) { $ContentType } else { $extension = [System.IO.Path]::GetExtension($FilePath).ToLower() switch ($extension) { '.pdf' { 'application/pdf' } '.doc' { 'application/msword' } '.docx' { 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' } '.xls' { 'application/vnd.ms-excel' } '.xlsx' { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' } '.png' { 'image/png' } '.jpg' { 'image/jpeg' } '.jpeg' { 'image/jpeg' } '.gif' { 'image/gif' } '.txt' { 'text/plain' } '.csv' { 'text/csv' } default { 'application/octet-stream' } } } # Build multipart form data $boundary = [System.Guid]::NewGuid().ToString() $headers['Content-Type'] = "multipart/form-data; boundary=$boundary" $bodyLines = @() $bodyLines += "--$boundary" $bodyLines += "Content-Disposition: form-data; name=`"file`"; filename=`"$actualFileName`"" $bodyLines += "Content-Type: $fileContentType" $bodyLines += "" $headerBytes = [System.Text.Encoding]::UTF8.GetBytes(($bodyLines -join "`r`n") + "`r`n") $footerBytes = [System.Text.Encoding]::UTF8.GetBytes("`r`n--$boundary--`r`n") # Add additional form fields from Body if present $additionalFields = @() if ($null -ne $Body) { foreach ($key in $Body.Keys) { $additionalFields += "--$boundary" $additionalFields += "Content-Disposition: form-data; name=`"$key`"" $additionalFields += "" $additionalFields += $Body[$key].ToString() } $additionalFields += "" } $additionalBytes = if ($additionalFields.Count -gt 0) { [System.Text.Encoding]::UTF8.GetBytes(($additionalFields -join "`r`n") + "`r`n") } else { @() } # Combine all parts $bodyStream = New-Object System.IO.MemoryStream $bodyStream.Write($headerBytes, 0, $headerBytes.Length) $bodyStream.Write($fileBytes, 0, $fileBytes.Length) if ($additionalBytes.Length -gt 0) { $bodyStream.Write($additionalBytes, 0, $additionalBytes.Length) } $bodyStream.Write($footerBytes, 0, $footerBytes.Length) $requestParams.Body = $bodyStream.ToArray() $requestParams.Headers = $headers } elseif ($null -ne $Body -and $Method -ne 'GET' -and $Method -ne 'HEAD') { # Regular JSON body $headers['Content-Type'] = 'application/json; charset=utf-8' $requestParams.Body = ($Body | ConvertTo-Json -Depth 10 -Compress) $requestParams.Headers = $headers Write-Verbose "Request body: $($requestParams.Body)" } try { # Make the request if ($Method -eq 'HEAD') { # HEAD requests - we need response headers $response = Invoke-WebRequest @requestParams $responseHeaders = @{} foreach ($key in $response.Headers.Keys) { $responseHeaders[$key] = $response.Headers[$key] } if ($ReturnHeaders) { return [PSCustomObject]@{ Data = $null Headers = $responseHeaders } } return $responseHeaders } else { # Use Invoke-WebRequest to get access to headers $response = Invoke-WebRequest @requestParams # Parse headers $responseHeaders = @{} foreach ($key in $response.Headers.Keys) { $responseHeaders[$key] = $response.Headers[$key] } # Parse response content $data = $null if ($response.Content) { if ($RawResponse) { $data = $response.Content } else { try { $data = $response.Content | ConvertFrom-Json } catch { # Response is not JSON $data = $response.Content } } } if ($ReturnHeaders) { return [PSCustomObject]@{ Data = $data Headers = $responseHeaders } } return $data } } catch { $statusCode = 0 $errorResponse = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode try { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $errorResponse = $reader.ReadToEnd() $reader.Close() try { $errorResponse = $errorResponse | ConvertFrom-Json } catch { # Keep as string } } catch { $errorResponse = $_.Exception.Message } } else { $errorResponse = $_.Exception.Message } $errorRecord = ConvertTo-UKGError -Response $errorResponse -StatusCode $statusCode -Exception $_.Exception $PSCmdlet.ThrowTerminatingError($errorRecord) } } function Update-UKGToken { <# .SYNOPSIS Refreshes the OAuth access token. .DESCRIPTION Uses stored credentials to obtain a new access token when the current one expires. #> [CmdletBinding()] param() if ($null -eq $Script:UKGSession) { throw "No active session. Use Connect-UKG first." } $tokenUrl = $Script:UKGSession.TokenUrl # Decrypt credentials $appId = $Script:UKGSession.ApplicationId $appSecretSecure = $Script:UKGSession.ApplicationSecret $clientId = $Script:UKGSession.ClientId # Convert SecureString to plain text if ($Script:PSVersionMajor -ge 7) { $appSecret = ConvertFrom-SecureString -SecureString $appSecretSecure -AsPlainText } else { $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($appSecretSecure) try { $appSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } $body = @{ grant_type = 'client_credentials' client_id = $clientId client_secret = "${appId}:${appSecret}" } $requestParams = @{ Uri = $tokenUrl Method = 'POST' Body = $body ContentType = 'application/x-www-form-urlencoded' UseBasicParsing = $true ErrorAction = 'Stop' } try { $response = Invoke-RestMethod @requestParams # Update session with new token $Script:UKGSession.AccessToken = $response.access_token $Script:UKGSession.TokenExpiry = (Get-Date).AddSeconds($response.expires_in) Write-Verbose "Token refreshed successfully. New expiry: $($Script:UKGSession.TokenExpiry)" } catch { throw "Failed to refresh token: $_" } } |