plugins/Jokes/Public/JokesAPI/Add-BulkJob.ps1



function Add-BulkJob {

<#
    .SYNOPSIS
        Import or export data from/to Salesforce using the BULK API v2
 
    .DESCRIPTION
 
        This function uses the Salesforce BULK API v2 to query or ingest data from/into Salesforce.
        It can be easily used to import and export data.
 
    .PARAMETER Object
        The salesforce object to ingest data into
 
    .PARAMETER Operation
        One of the following operations when ingesting data: insert|delete|hardDelete|update|upsert
 
    .PARAMETER Query
        The SOQL query to run, not used for ingesting data
 
    .PARAMETER QueryOperation
        Either request the not deleted data or all data. Allowed values: query|queryAll
 
    .PARAMETER LineEnding
        Depends on the operating system that created the file for ingesting data
        Or that receives the data
        Allowed values are LF (Linux) and CRLF (Windows)
 
    .PARAMETER ColumnDelimiter
        Typical csv delimiters, allowed are: BACKQUOTE|CARET|COMMA|PIPE|SEMICOLON|TAB
 
    .PARAMETER ExternalIdFieldName
        Upserting records requires an external ID field on the object involved in the job
 
    .PARAMETER Path
        The file to be uploaded, this does not support splitting, so be aware to have this file < 150M after base64 encoding
 
    .PARAMETER CheckSeconds
        Check the job status every n seconds
 
    .PARAMETER MaxSecondsWait
        Maximum wait time for the job
 
    .PARAMETER DownloadFailures
        Switch to generally download failes into a temporary file
 
    .PARAMETER FailureFilename
        The file to write the file to, when $DownloadFailures is true and there are failures
 
    .PARAMETER DownloadSuccessful
        Switch to generally download successful items into a temporary file
 
    .PARAMETER SuccessfulFilename
        The file to write the file to, when $DownloadSuccessful is true and there are successful records
 
    .PARAMETER DownloadUnprocessed
        Switch to generally download unprocessed items into a temporary file
 
    .PARAMETER UnprocessedFilename
        The file to write the file to, when $DownloadUnprocessed is true and there are unprocessed records
 
    .EXAMPLE
        Get all IDs from an object and delete all records
 
        $tempFile = "c:\temp\tempfile.csv"
        $d = Invoke-SFSCQuery -Query "Select Id from CampaignMember" -bulk
        $l = $d | Select Id | convertto-csv -Delimiter "`t" -NoTypeInformation
        [IO.File]::WriteAllLines($tempFile, $l) # Write the file with BOM
        Add-BulkJob -Object CampaignMember -Operation delete -ColumnDelimiter TAB -LineEnding CRLF -Path $tempFile
        Invoke-SFSCQuery -Query "Select count() from CampaignMember"
 
 
    .EXAMPLE
        Building up job parameters and then execute the job.
 
        $successfulFilename =
        $lJobParams = [Hashtable]@{
            "Object" = "Lead"
            "Path" = "C:\temp\leads.csv"
            "Operation" = "upsert"
            "CheckSeconds" = 20
            "MaxSecondsWait" = 4000
            "DownloadSuccessful" = $True
            "SuccessfulFilename" = ( Join-Path -Path $Env:tmp -ChildPath "successful_$( [guid]::newguid().toString() ).csv" )
            "ExternalIdFieldName" = "apteco__externalId__c"
            "DownloadFailures" = $True
            "FailureFilename" = ( Join-Path -Path $Env:tmp -ChildPath "failed_$( [guid]::newguid().toString() ).csv" )
        }
        $lJob = Add-BulkJob @lJobParams
 
 
    .EXAMPLE
        Query data via bulk job
 
        $bulkParams = [Hashtable]@{
            "Query" = "Select id, name from Contact"
            "Path" = ".\newData.csv"
            "QueryOperation" = "query"
        }
        $return = Add-BulkJob @bulkParams
 
    .INPUTS
        None. You cannot pipe objects to this function.
 
    .OUTPUTS
        Array of jobs
 
    .NOTES
        Author: florian.von.bracht@apteco.de
 
    #>


    [CmdletBinding(DefaultParameterSetName = 'Ingest')]
    param (

        #[Parameter(Mandatory=$false)][Hashtable] $InputHashtable
         [Parameter(Mandatory=$True, ParameterSetName = 'Ingest')]
         [String]$Object

        ,[Parameter(Mandatory=$True, ParameterSetName = 'Query')]
         [String]$Query

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [ValidateSet("insert", "delete", "hardDelete", "update", "upsert", IgnoreCase = $false)]
         [String]$Operation = "insert"             # insert|delete|hardDelete|update|upsert

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Query')]
         [ValidateSet("query","queryAll", IgnoreCase = $false)]
         [String]$QueryOperation = "query"             # query|queryAll

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [Parameter(Mandatory=$False, ParameterSetName = 'Query')]
         [ValidateSet("CRLF", "LF", IgnoreCase = $false)]
         [String]$LineEnding = "CRLF"             # LF (Linux) and CRLF (Windows)

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [Parameter(Mandatory=$False, ParameterSetName = 'Query')]
         [ValidateSet("BACKQUOTE", "CARET", "COMMA", "PIPE", "SEMICOLON", "TAB", IgnoreCase = $false)]
         [String]$ColumnDelimiter = "TAB"             # BACKQUOTE|CARET|COMMA|PIPE|SEMICOLON|TAB

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [String]$ExternalIdFieldName = ""

        # TODO implement splitting of multiple files and return an array of jobs rather than one job -> currently done in the calling script
        ,[Parameter(Mandatory=$True, ParameterSetName = 'Ingest')]
         [Parameter(Mandatory=$True, ParameterSetName = 'Query')]
         [String]$Path = ""

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [Parameter(Mandatory=$False, ParameterSetName = 'Query')]
         [Int]$CheckSeconds = 15

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [Parameter(Mandatory=$False, ParameterSetName = 'Query')]
         [Int]$MaxSecondsWait = 3000

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [Switch]$DownloadFailures = $false

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [String]$FailureFilename = ""

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [Switch]$DownloadSuccessful = $false

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [String]$SuccessfulFilename = ""

         ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [Switch]$DownloadUnprocessed = $false

        ,[Parameter(Mandatory=$False, ParameterSetName = 'Ingest')]
         [String]$UnprocessedFilename = ""

    )

    begin {

        #-----------------------------------------------
        # NOTES
        #-----------------------------------------------

        <#
 
        #>


        #-----------------------------------------------
        # CHECK THE INPUT FILE
        #-----------------------------------------------

        # Resolve the filename to an absolute path
        $absolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)


        If ($PSCmdlet.ParameterSetName -eq "Ingest") {

            If ( ( Test-Path -Path $absolutePath -IsValid ) -eq $True ) {

                If ( ( Test-Path -Path $absolutePath ) -eq $True ) {
                    # path is valid
                } else {
                    throw "Path '$( $absolutePath )' is not existing"
                }

            } else {
                throw "Path '$( $absolutePath )' is not valid"
            }
        }


        #-----------------------------------------------
        # CHECK THE OUTPUT FILE
        #-----------------------------------------------

        If ($PSCmdlet.ParameterSetName -eq "Query") {

            # Check the filename
            If ( ( Test-Path -Path $absolutePath -IsValid -PathType Leaf ) -eq $True ) {
                # Filepath seems to be allowed
                #Split-path $p -Parent
            } else {
                throw "Path '$( $absolutePath )' is not valid"
            }

        }


        #-----------------------------------------------
        # CHECK THE FILES TO DOWNLOAD
        #-----------------------------------------------

        If ( $DownloadFailures -eq $True ) {

            # Create a default filename, if empty
            If ( $FailureFilename -eq "" ) {
                ".\failed_$( [guid]::newguid.ToString() )_$( [datetime]::now.toString("yyyyMMdd_HHmmss") ).csv"
            }

            # Resolve the filename to an absolute path
            $failAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FailureFilename)

            # Check the filename
            If ( ( Test-Path -Path $failAbsolutePath -IsValid -PathType Leaf ) -eq $True ) {
                # Filepath seems to be allowed
                #Split-path $p -Parent
            } else {
                throw "Path '$( $failAbsolutePath )' is not valid"
            }

        }

        If ( $DownloadSuccessful -eq $True ) {

            # Create a default filename, if empty
            If ( $SuccessfulFilename -eq "" ) {
                ".\successful_$( [guid]::newguid.ToString() )_$( [datetime]::now.toString("yyyyMMdd_HHmmss") ).csv"
            }

            # Resolve the filename to an absolute path
            $succAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SuccessfulFilename)

            # Check the filename
            If ( ( Test-Path -Path $succAbsolutePath -IsValid -PathType Leaf ) -eq $True ) {
                # Filepath seems to be allowed
                #Split-path $p -Parent
            } else {
                throw "Path '$( $succAbsolutePath )' is not valid"
            }

        }

        If ( $DownloadUnprocessed -eq $True ) {

            # Create a default filename, if empty
            If ( $UnprocesssedFilename -eq "" ) {
                ".\unprocesssed_$( [guid]::newguid.ToString() )_$( [datetime]::now.toString("yyyyMMdd_HHmmss") ).csv"
            }

            # Resolve the filename to an absolute path
            $unpAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($UnprocesssedFilename)

            # Check the filename
            If ( ( Test-Path -Path $unpAbsolutePath -IsValid -PathType Leaf ) -eq $True ) {
                # Filepath seems to be allowed
                #Split-path $p -Parent
            } else {
                throw "Path '$( $unpAbsolutePath )' is not valid"
            }

        }


        #-----------------------------------------------
        # SOME SETTINGS
        #-----------------------------------------------


    }

    process {

        #-----------------------------------------------
        # DEFINE DATA
        #-----------------------------------------------

        <#
 
        # COLUMN NAMES
 
        For a standard field, use the Field Name value as the field column header in your CSV file.
        For a custom field, use the API Name value as the field column header in a CSV file or the field name identifier in an XML or JSON file. (To find the API Name, click the field name.)
 
        # PARENT ENTRIES
 
        You can use a child-to-parent relationship, but you can't use a parent-to-child relationship.
        You can use a child-to-parent relationship, but you can't extend it to use a child-to-parent-grandparent relationship.
        You can only use indexed fields on the parent object. A custom field is indexed if its External ID field is selected. A standard field is indexed if its idLookup property is set to true. See the Field Properties column in the field table for each standard object.
 
        #>


        #-----------------------------------------------
        # CHECK INPUT FILES AND SPLIT THEM IF TOO BIG
        #-----------------------------------------------

        <#
 
        To fulfill the maximum filesize of 150MB after base64 (enlarges around 33%), the file shouldn't be
        larger than 112 MB.
 
        A file with 4 columns with a width of 20 characters and TAB delimiter can contain around 1.2M rows.
 
        Do not forget that the failure files should also get an increment in their filename
 
        #>



        #-----------------------------------------------
        # CREATE JOB
        #-----------------------------------------------

        <#
 
        # REFERENCE
 
        https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/create_job.htm
 
        # EXTERNAL ID
 
        (Copied from the SF Help)
 
        Confirm that your object is using an external ID field.
 
        Upserting records requires an external ID field on the object involved in the job. Bulk API 2.0 uses the external ID field to determine whether a record is used to update an existing record or create a record.
        This example assumes that the external ID field customExtIdField__c has been added to the Account object.
        To add this custom field in your org with Object Manager, use these properties.
 
            Data Type—text
            Field Label—customExtIdField
            Select External ID
 
        #>


        switch ($PSCmdlet.ParameterSetName) {

            "Ingest" {

                $jobDetails = [PSCustomObject]@{
                    #"assignmentRuleId" = ""
                    "object" = $Object     # Single object per job
                    "contentType" = "CSV"           # CSV - No more options available
                    "operation" = $Operation          # insert|delete|hardDelete|update|upsert
                    "lineEnding" = $LineEnding         # LF (Linux) and CRLF (Windows)
                    "columnDelimiter" = $ColumnDelimiter     # BACKQUOTE|CARET|COMMA|PIPE|SEMICOLON|TAB
                }

                # Required for upsert, something like customExtIdField__c
                If ( $ExternalIdFieldName -ne "") {
                    $jobDetails | Add-Member -MemberType NoteProperty -Name "externalIdFieldName" -Value $ExternalIdFieldName
                }

                $job = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/" -Method "POST" -Body $jobDetails

                break
            }

            "Query" {

                $jobDetails = [PSCustomObject]@{
                    #"assignmentRuleId" = ""
                    "query" = $Query     # Single object per job
                    "contentType" = "CSV"           # CSV - No more options available
                    "operation" = $QueryOperation          # query|queryAll
                    "lineEnding" = $LineEnding         # LF (Linux) and CRLF (Windows)
                    "columnDelimiter" = $ColumnDelimiter     # BACKQUOTE|CARET|COMMA|PIPE|SEMICOLON|TAB
                }

                $job = Invoke-SFSC -Service "data" -Object "jobs" -Path "/query/" -Method "POST" -Body $jobDetails

            }

        }


        # curl https://MyDomainName.my.salesforce.com/services/data/v58.0/jobs/ingest/ -H 'Authorization: Bearer 00DE0X0A0M0PeLE!AQcAQH0dMHEXAMPLEzmpkb58urFRkgeBGsxL_QJWwYMfAbUeeG7c1EXAMPLEDUkWe6H34r1AAwOR8B8fLEz6nEXAMPLE' -H "Content-Type: application/json" -H "Accept: application/json" -H "X-PrettyPrint:1" -d @newinsertjob.json -X POST
        #$jobDetailsJson = ConvertTo-Json $jobDetails
        #$job = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/" -Method POST -verbose -ContentType $contentType -Headers $headers -body $jobDetailsJson

        # ingest return
        <#
 
        { "id" : "7505fEXAMPLE4C2AAM",​
        "operation" : "insert",​
        "object" : "Account",​
        "createdById" : "0055fEXAMPLEtG4AAM",​
        "createdDate" : "2022-01-02T21:33:43.000+0000",​
        "systemModstamp" : "2022-01-02T21:33:43.000+0000",​
        "state" : "Open",​
        "concurrencyMode" : "Parallel",​
        "contentType" : "CSV",​
        "apiVersion" : 58.0,​
        "contentUrl" : "services/data/58.0/jobs/ingest/7505fEXAMPLE4C2AAM/batches",​
        "lineEnding" : "LF",​ "columnDelimiter" : "COMMA" }
 
        #>


        Write-Log "Created job with id '$( $job.id )'"


        #-----------------------------------------------
        # SAVE THE LAST JOB ID
        #-----------------------------------------------

        If ( $Script:variableCache.Keys -contains "last_jobid" ) {
            $Script:variableCache.last_jobid = $job.id
        } else {
            $Script:variableCache.Add("last_jobid", $job.id)
        }


        #-----------------------------------------------
        # UPLOAD THE DATA
        #-----------------------------------------------

        # MAX 150M after Base64 encoding
        # curl https://MyDomainName.my.salesforce.com/services/data/v58.0/jobs/ingest/7505fEXAMPLE4C2AAM/batches/ -H 'Authorization: Bearer 00DE0X0A0M0PeLE!AQcAQH0dMHEXAMPLEzmpkb58urFRkgeBGsxL_QJWwYMfAbUeeG7c1EXAMPLEDUkWe6H34r1AAwOR8B8fLEz6nEXAMPLE' -H "Content-Type: text/csv" -H "Accept: application/json" -H "X-PrettyPrint:1" --data-binary @bulkinsert.csv -X PUT

        #$upload = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/$( $job.id )/batches/" -Method PUT -verbose -ContentType "text/csv" -Headers $headers -body $accountsCsv

        # TODO Switch to multipart upload for better performance
        # When using pwsh, this is supported by the -form parameter: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-restmethod?view=powershell-7.5#parameters

        If ($PSCmdlet.ParameterSetName -eq "Ingest") {

            $upload = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/batches/" -Method "PUT" -ContentType "text/csv" -InFile $Path #$file.FullName

            Write-Log "Did the upload for job '$( $job.id )'"

        }


        #-----------------------------------------------
        # SET STATE COMPLETE
        #-----------------------------------------------

        # curl https://MyDomainName.my.salesforce.com/services/data/v58.0/jobs/ingest/7505fEXAMPLE4C2AAM/ -H 'Authorization: Bearer 00DE0X0A0M0PeLE!AQcAQH0dMHEXAMPLEzmpkb58urFRkgeBGsxL_QJWwYMfAbUeeG7c1EXAMPLEDUkWe6H34r1AAwOR8B8fLEz6nEXAMPLE' -H "Content-Type: application/json; charset=UTF-8" -H "Accept: application/json" -H "X-PrettyPrint:1" --data-raw '{ "state" : "UploadComplete" }' -X PATCH
        # $patchDetails = [PSCustomObject]@{
        # "state" = "UploadComplete"
        # }
        # $patchedJob = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/$( $job.id )/" -Method PATCH -verbose -ContentType $contentType -Headers $headers -body $patchDetails

        If ($PSCmdlet.ParameterSetName -eq "Ingest") {

            $uploadCompleteBody = [PSCustomObject]@{
                "state" = "UploadComplete"
            }
            $patchedJob = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/" -Method "PATCH" -body $uploadCompleteBody

            Write-Log "Patched the job '$( $job.id )' to state 'UploadComplete'"

        }

        <#
 
        { "id" : "7505fEXAMPLE4C2AAM",​
        "operation" : "insert",​
        "object" : "Account",​
        "createdById" : "0055fEXAMPLEtG4AAM",​
        "createdDate" : "2022-01-02T21:33:43.000+0000",​
        "systemModstamp" : "2022-01-02T21:33:43.000+0000",​
        "state" : "UploadComplete",​
        "concurrencyMode" : "Parallel",​
        "contentType" : "CSV",​
        "apiVersion" : 58.0 }
 
        #>



        #-----------------------------------------------
        # CHECK JOB STATUS ASYNC
        #-----------------------------------------------

        # curl https://MyDomainName.my.salesforce.com/services/data/v58.0/jobs/ingest/7505fEXAMPLE4C2AAM/ -H 'Authorization: Bearer 00DE0X0A0M0PeLE!AQcAQH0dMHEXAMPLEzmpkb58urFRkgeBGsxL_QJWwYMfAbUeeG7c1EXAMPLEDUkWe6H34r1AAwOR8B8fLEz6nEXAMPLE' -H "Accept: application/json" -H "X-PrettyPrint:1" -X GET
        $jobStartTs = [datetime]::now
        Do {

            Start-Sleep -seconds $CheckSeconds

            #$jobStatus = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/$( $job.id )/" -Method GET -verbose -ContentType $contentType -Headers $headers
            switch ($PSCmdlet.ParameterSetName) {

                "Ingest" {
                    $jobStatus = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/" -Method "GET"
                    break
                }

                "Query" {
                    $jobStatus = Invoke-SFSC -Service "data" -Object "jobs" -Path "/query/$( $job.id )/" -Method "GET"
                }

            }
            Write-Log " Job status: $( $jobStatus.state ) - $( $jobStatus.numberRecordsProcessed ) records done - $( $jobStatus.numberRecordsFailed ) records failed"

            $jobTs = New-TimeSpan -Start $jobStartTs -End ( [datetime]::now )

        } Until ( @("Failed", "JobComplete", "Aborted") -contains $jobStatus.state -or $jobTs.TotalSeconds -gt $MaxSecondsWait )

        If ( $jobStatus.state -ne "JobComplete" ) {
            Write-Log -Severity ERROR -Message "Job $( $job.id ) with status '$( $jobStatus.state )': '$( $jobStatus.errorMessage )'"
            throw "$( $jobStatus.errorMessage )"
        } else {
            Write-Log -Severity VERBOSE -Message "Job $( $job.id ) with status '$( $jobStatus.state )':"
            Write-Log -Severity VERBOSE -Message " retries: $( $jobStatus.retries )"
            Write-Log -Severity VERBOSE -Message " totalProcessingTime: $( $jobStatus.totalProcessingTime )"
        }

        <#
 
        {
            "id": "750FS00000FLBJDYA5",
            "operation": "delete",
            "object": "CampaignMember",
            "createdById": "005FS00000Jg9p2YAB",
            "createdDate": "2025-03-04T17:48:37.000+0000",
            "systemModstamp": "2025-03-04T17:48:41.000+0000",
            "state": "Failed",
            "concurrencyMode": "Parallel",
            "contentType": "CSV",
            "apiVersion": 58.0,
            "jobType": "V2Ingest",
            "lineEnding": "CRLF",
            "columnDelimiter": "TAB",
            "numberRecordsProcessed": 0,
            "numberRecordsFailed": 0,
            "retries": 0,
            "totalProcessingTime": 0,
            "apiActiveProcessingTime": 0,
            "apexProcessingTime": 0,
            "errorMessage": "InvalidBatch : The \u0027delete\u0027 batch must contain only ids"
        }
 
 
        #>



        #-----------------------------------------------
        # GET RESULTS
        #-----------------------------------------------

        #$jobResults = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/" -method get
        $processed = $jobStatus.numberRecordsProcessed
        $failed = $jobStatus.numberRecordsFailed
        $successful = $processed - $failed

        $returnHashtable = [Hashtable]@{
            "jobid" = $job.id
            "processed" = $processed
            "failed" = $failed
            "successful" = $successful
        }

        # Write the failed results into a file
        If ( $DownloadFailures -eq $True -and $failed -gt 0 ) {
            $fails = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/failedResults" -method get #-outfile $failAbsolutePath #| Set-Content -Path $failAbsolutePath
            $fails | Export-Csv $failAbsolutePath -Encoding UTF8 -NoTypeInformation -Delimiter "`t"
            Write-Log "Written failures to '$( $failAbsolutePath )'" -severity VERBOSE
            $returnHashtable.add("failureFile", $failAbsolutePath)
            $returnHashtable.add("failureObj", $fails)
        }

        # Write the successful results into a file
        If ( $DownloadSuccessful -eq $True -and $successful -gt 0 ) {
            $succ = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/successfulResults" -method get #-outfile $succAbsolutePath #| Set-Content -Path $succAbsolutePath
            $succ | Export-Csv $succAbsolutePath -Encoding UTF8 -NoTypeInformation -Delimiter "`t"
            Write-Log "Written successful to '$( $succAbsolutePath )'" -severity VERBOSE
            $returnHashtable.add("successfulFile", $succAbsolutePath)
            $returnHashtable.add("successfulObj", $succ)
        }

        # Write the unprocessed restults into a file (only when canceled or aborted)
        If ( $DownloadUnprocessed -eq $True ) {
            $unp = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/unprocessedRecords" -method get #-outfile $unpAbsolutePath #| Set-Content -Path $unpAbsolutePath
            $unp | Export-Csv $unpAbsolutePath -Encoding UTF8 -NoTypeInformation -Delimiter "`t"
            Write-Log "Written unprocessed to '$( $unpAbsolutePath )'" -severity VERBOSE
            $returnHashtable.add("unprocessedFile", $unpAbsolutePath)
            $returnHashtable.add("unProcessedObj", $unp)

        }

        # Download file via paging
        If ( $PSCmdlet.ParameterSetName -eq "Query") {
            $data = Invoke-SFSC -Service "data" -Object "jobs" -Path "/query/$( $job.id )/results" -Query ( [PSCustomObject]@{ "maxRecords" = $Script:settings.upload.maxRecordsPerPageBulkDownload } ) -method get -headers ( [Hashtable]@{ "Accept-Encoding" = "gzip" } )
        }


        #-----------------------------------------------
        # RETURN
        #-----------------------------------------------

        If ( $PSCmdlet.ParameterSetName -eq "Query") {
            $data
        } else {
            [Array]@(
                $returnHashtable
            )
        }


    }

    end {

    }

}