plugins/SalesforceSC/Public/peoplestage/invoke-upload.ps1
function Invoke-Upload{ [CmdletBinding()] param ( [Parameter(Mandatory=$false)][Hashtable] $InputHashtable ) begin { #----------------------------------------------- # START TIMER #----------------------------------------------- $processStart = [datetime]::now #$inserts = 0 #----------------------------------------------- # LOG #----------------------------------------------- $moduleName = "UPLOAD" # Start the log Write-Log -message $Script:logDivider Write-Log -message $moduleName -Severity INFO # Log the params, if existing Write-Log -message "INPUT:" if ( $InputHashtable ) { $InputHashtable.Keys | ForEach-Object { $param = $_ Write-Log -message " $( $param ) = '$( $InputHashtable[$param] )'" -writeToHostToo $false } } #----------------------------------------------- # DEBUG MODE #----------------------------------------------- Write-Log "Debug Mode: $( $Script:debugMode )" #----------------------------------------------- # PARSE MESSAGE #----------------------------------------------- #$script:debug = $InputHashtable $uploadOnly = $false # TODO add an option to turn off tagging for upload only If ( "" -eq $InputHashtable.MessageName ) { #Write-Log "A" $uploadOnly = $true $mailing = [Mailing]::new(999, "UploadOnly") } else { #Write-Log "B" Write-Log "Parsing message: '$( $InputHashtable.MessageName )' with '$( $Script:settings.nameConcatChar )' as separator" $mailing = [Mailing]::new($InputHashtable.MessageName) Write-Log "Got chosen message entry with id '$( $mailing.mailingId )' and name '$( $mailing.mailingName )'" #$mailing = [Mailing]::new($InputHashtable.MessageName) #Write-Log "Got chosen message entry with id '$( $mailing.mailingId )' and name '$( $mailing.mailingName )'" } #----------------------------------------------- # CHECK INPUT FILE #----------------------------------------------- # Checks input file automatically $file = Get-Item -Path $InputHashtable.Path Write-Log -Message "Got a file at $( $file.FullName )" # Add note in log file, that the file is a converted file if ( $file.FullName -match "\.converted$") { Write-Log -message "Be aware, that the exports are generated in Codepage 1252 and not UTF8. Please change this in the Channel Editor." -severity ( [LogSeverity]::WARNING ) } # Count the rows $rowsCount = 0 # [ ] if this needs to much performance, this is not needed If ( $Script:settings.upload.countRowsInputFile -eq $true ) { $rowsCount = Measure-Rows -Path $file.FullName -SkipFirstRow Write-Log -Message "Got a file with $( $rowsCount ) rows" } else { Write-Log -Message "RowCount of input file not activated" } #throw [System.IO.InvalidDataException] $msg #Write-Log -Message "Debug Mode: $( $Script:debugMode )" #----------------------------------------------- # CHECK CLEVERREACH CONNECTION #----------------------------------------------- <# try { Test-CleverReachConnection } catch { #$msg = "Failed to connect to CleverReach, unauthorized or token is expired" #Write-Log -Message $msg -Severity ERROR Write-Log -Message $_.Exception -Severity ERROR throw [System.IO.InvalidDataException] $msg exit 0 } #Write-Log -Message "Debug Mode: $( $Script:debugMode )" #> } process { try { #----------------------------------------------- # CREATE GROUP IF NEEDED #----------------------------------------------- # If lists contains a concat character (id+name), use the list id # if no concat character is present, take the whole string as name for a new list and search for it... if not present -> new list! # if no list is present, just take the current date and time # If listname is valid -> contains an id, concatenation character and and a name -> use the id <# try { $createNewGroup = $false # No need for the group creation now $list = [MailingList]::new($InputHashtable.ListName) $listName = $list.mailingListName $groupId = $list.mailingListId Write-Log "Got chosen list/group entry with id '$( $list.mailingListId )' and name '$( $list.mailingListName )'" # Asking for details and possibly throw an exception $g = Invoke-CR -Object "groups" -Path "/$( $groupId )" -Method GET -Verbose } catch { # Listname is the same as the message means nothing was entered -> check the name if ( $InputHashtable.ListName -ne $InputHashtable.MessageName ) { # Try to search for that group and select the first matching entry or throw exception $groups = Invoke-CR -Object "groups" -Method "GET" -Verbose # Check how many matches are available $matchingGroups = @( $groups | where-object { $_.name -eq $InputHashtable.ListName } ) # put an array around because when the return is one object, it will become a pscustomobject switch ( $matchingGroups.Count ) { # No match -> new group 0 { $createNewGroup = $true $listName = $InputHashtable.ListName Write-Log -message "No matched group -> create a new one" -severity INFO } # One match -> use that one! 1 { $createNewGroup = $false # No need for the group creation now $listName = $matchingGroups.name $groupId = $matchingGroups.id Write-Log -message "Matched one group -> use that one" -severity INFO } # More than one match -> throw exception Default { $createNewGroup = $false # No need for the group creation now Write-Log -message "More than one match -> throw exception" -severity ERROR throw [System.IO.InvalidDataException] "More than two groups with that name. Please choose a unique list." } } # String is empty, create a generic group name } else { $createNewGroup = $true $listName = [datetime]::Now.ToString("yyyyMMdd_HHmmss") Write-Log -message "Create a new group with a timestamp" -severity INFO } } # Create a new group (if needed) if ( $createNewGroup -eq $true ) { $body = [PSCustomObject]@{ "name" = "$( $listName )" } $newGroup = Invoke-CR -Object "groups" -Body $body -Method "POST" -Verbose $groupId = $newGroup.id Write-Log -message "Created a new group with id $( $groupId )" -severity INFO } #> #----------------------------------------------- # CHECK CAMPAIGN #----------------------------------------------- # TODO [ ] put into docs, that I need a idlookup field that simply mirrors the id field $campaignId = $mailing.mailingId $campaign = Invoke-SFSCQuery -Query "Select Id, Name, idlookup__c from Campaign where idlookup__c like '%$( $campaignId.substring(0,$campaignId.length-3) )%'" Write-Log "Using salesforce campaign '$( $campaign.Name )' with id '$( $campaign.id )'" #----------------------------------------------- # CHECK CAMPAIGN MEMBER STATUS #----------------------------------------------- $list = [MailingList]::new($InputHashtable.ListName) $listName = $list.mailingListName $listId = $list.mailingListId #$campaignMemberStatus = Invoke-SFSCQuery -Query "Select Id, Label from CampaignMemberStatus where Name = '$( $listname )' and CampaignId = '$( $campaign.Id )'" #Write-Log "Using salesforce campaign member status '$( $campaignMemberStatus.Label )' with id '$( $campaignMemberStatus.Id )'" Write-Log "Using salesforce campaign member status '$( $listName )'" #----------------------------------------------- # 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. #> #----------------------------------------------- # 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 #> $jobDetails = [PSCustomObject]@{ #"assignmentRuleId" = "" "object" = "CampaignMember" # Single object per job "contentType" = "CSV" # CSV - No more options available "operation" = "insert" # insert|delete|hardDelete|update|upsert "lineEnding" = "CRLF" # LF (Linux) and CRLF (Windows) "columnDelimiter" = "TAB" # BACKQUOTE|CARET|COMMA|PIPE|SEMICOLON|TAB #"externalIdFieldName" = "" # Required for upsert, something like customExtIdField__c } # 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 $job = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/" -Method "POST" -Body $jobDetails #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 )'" #----------------------------------------------- # TRANSFORM THE DATA #----------------------------------------------- # TODO [ ] maybe scale this up in future or think about multipart upload # TODO [ ] check if ContactID or LeadID is present $sfFields = Get-SFSCObjectField -Object "CampaignMember" $csv = @( Import-csv -Delimiter "`t" -Path $file.FullName -Encoding UTF8 ) $newCsv = [System.Collections.ArrayList]@() $csv | ForEach-Object { $row = $_ $line = [PSCustomObject]@{ "CampaignID" = $campaign.id "Status" = $list.mailingListId } $row.psobject.properties | ForEach-Object { $prop = $_ If ( $sfFields.name -contains $prop.name ) { $line | Add-Member -MemberType NoteProperty -Name $prop.name -Value $prop.value } } [void]$newCsv.add($line) } Write-Log "Converted $( $newCsv.count ) lines" $nf = Join-Path -Path $Env:tmp -ChildPath "$( [guid]::newguid().toString() ).csv" #New-TemporaryFile Write-Log "Using temporary file $( $nf )" # TODO [ ] Not the best way when you have quotes in values $newCsvContent = $newCsv | convertto-csv -NoTypeInformation -Delimiter "`t" | ForEach-Object { $_ -replace '"','' } #$newCsvContent | set-content -Path $nf -Encoding UTF8 [IO.File]::WriteAllLines($nf, $newCsvContent) Write-Log "File is written" #----------------------------------------------- # 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 $upload = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/batches/" -Method "PUT" -ContentType "text/csv" -InFile $nf #$file.FullName Write-Log "Did the upload" #----------------------------------------------- # 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 $patchedJob = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/" -Method "PATCH" -body ( [PSCustomObject]@{ "state" = "UploadComplete" } ) Write-Log "Patched the job 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 Do { #$jobStatus = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/$( $job.id )/" -Method GET -verbose -ContentType $contentType -Headers $headers $jobStatus = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/" -Method "GET" Write-Log "Job status: $( $jobStatus.state )" #$jobStatus.state Start-Sleep -seconds 10 } Until ( @("Failed", "JobComplete") -contains $jobStatus.state) # TODO [ ] add a timer like in CleverReach at the end of the broadcast #----------------------------------------------- # GET RESULTS #----------------------------------------------- # curl https://MyDomainName.my.salesforce.com/services/data/v58.0/jobs/ingest/7505fEXAMPLE4C2AAM/successfulResults/ -H 'Authorization: Bearer 00DE0X0A0M0PeLE!AQcAQH0dMHEXAMPLEzmpkb58urFRkgeBGsxL_QJWwYMfAbUeeG7c1EXAMPLEDUkWe6H34r1AAwOR8B8fLEz6nEXAMPLE' -H "Content-Type: application/json" -H "Accept: text/csv" -H "X-PrettyPrint:1" -X GET # failedResults # unprocessedRecords <# $csvHeaders = $headers.Clone() $csvHeaders.Accept = "text/csv" $success = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/$( $job.id )/successfulResults/" -Method GET -verbose -ContentType $contentType -Headers $csvHeaders $failed = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/$( $job.id )/failedResults/" -Method GET -verbose -ContentType $contentType -Headers $csvHeaders $unprocessed = Invoke-RestMethod -URI "$( $base )/services/data/v$( $version )/jobs/ingest/$( $job.id )/unprocessedRecords/" -Method GET -verbose -ContentType $contentType -Headers $csvHeaders #> $jobResults = Invoke-SFSC -Service "data" -Object "jobs" -Path "/ingest/$( $job.id )/" -method get $processed = $jobResults.numberRecordsProcessed $failed = $jobResults.numberRecordsFailed $successful = $processed - $failed Write-Log "$( $processed ) processed records" -severity INFO Write-Log "$( $failed ) failed" -severity INFO } catch { $msg = "Error during uploading data. Abort!" Write-Log -Message $msg -Severity ERROR -WriteToHostToo $false Write-Log -Message $_.Exception -Severity ERROR throw $_.Exception } finally { # Close the file reader, if open # If the variable is not already declared, that shouldn't be a problem # try { # $reader.Close() # } catch { # } #----------------------------------------------- # STOP TIMER #----------------------------------------------- $processEnd = [datetime]::now $processDuration = New-TimeSpan -Start $processStart -End $processEnd Write-Log -Message "Needed $( [int]$processDuration.TotalSeconds ) seconds in total" #Write-Host "Uploaded $( $j ) record. Confirmed $( $tagcount ) receivers with tag '$( $tags )'" } #----------------------------------------------- # RETURN VALUES TO PEOPLESTAGE #----------------------------------------------- # count the number of successful upload rows $recipients = $successful #$dataCsv.Count # TODO work out what to be saved # put in the source id as the listname $transactionId = $job.id #$Script:processId #$targetGroup.targetGroupId # TODO or try to log the used tag? # return object $return = [Hashtable]@{ # Mandatory return values "Recipients"=$recipients "TransactionId"=$transactionId # General return value to identify this custom channel in the broadcasts detail tables "CustomProvider"= $Script:settings.providername "ProcessId" = $Script:processId # More values for broadcast #"Tag" = ( $tags -join ", " ) #"GroupId" = $groupId #"PreheaderIsSet" = $preheaderIsSet # Some more information for the broadcasts script #"EmailFieldName"= $params.EmailFieldName #"Path"= $params.Path #"UrnFieldName"= $params.UrnFieldName #"TargetGroupId" = $targetGroup.targetGroupId # More information about the different status of the import #"RecipientsIgnored" = $status.report.total_ignored #"RecipientsQueued" = $recipients #"RecipientsSent" = $status.report.total_added + $status.report.total_updated } # log the return object Write-Log -message "RETURN:" $return.Keys | ForEach-Object { $param = $_ Write-Log -message " $( $param ) = '$( $return[$param] )'" -writeToHostToo $false } # return the results $return } end { } } |