forked from nimajalali/go-force
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/carecon 792 add Job and CheckJobStatus into go-force (#1)
* adds jobstatus check * adds job type definition * adds test for job
- Loading branch information
1 parent
34aaec2
commit 8f491ab
Showing
16 changed files
with
399 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package force | ||
|
||
import ( | ||
"net/http" | ||
) | ||
|
||
type OptionsFunc func(*Job) | ||
|
||
type BulkClient interface { | ||
Do(req *http.Request) (*http.Response, error) | ||
} | ||
|
||
type ObjectMapper func(objects any) [][]string | ||
|
||
type Job struct { | ||
info *JobInfo | ||
operation JobOperation | ||
forceApi *ForceApi | ||
objectMapper ObjectMapper | ||
client BulkClient | ||
apiVersion string | ||
} | ||
|
||
type JobInfo struct { | ||
Id string `json:"id"` | ||
State string `json:"state"` | ||
NumberRecordsFailed int `json:"numberRecordsFailed"` | ||
NumberRecordsProcessed int `json:"numberRecordsProcessed"` | ||
JobMessage string `json:"errorMessage"` | ||
ContentURL string `json:"contentUrl"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package force | ||
|
||
import ( | ||
"bytes" | ||
"encoding/csv" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
// CreateJob creates a new pointer to an instance of Job. Can be Modified with the given JobOptionsFuncs | ||
func CreateJob(fapi *ForceApi, opts ...OptionsFunc) *Job { | ||
job := &Job{ | ||
forceApi: fapi, | ||
operation: JobOperation{}, | ||
objectMapper: func(objects any) [][]string { return nil }, | ||
info: &JobInfo{}, | ||
apiVersion: DefaultAPIVersion, | ||
client: http.DefaultClient, | ||
} | ||
for _, opt := range opts { | ||
opt(job) | ||
} | ||
|
||
return job | ||
} | ||
|
||
// JobWithHTTPClient adds a HTTPClient to the Job, to communicate with salesforce | ||
func JobWithHTTPClient(client BulkClient) OptionsFunc { | ||
return func(job *Job) { | ||
job.client = client | ||
} | ||
} | ||
|
||
// JobWithJobInfo adds Job Information to the Job | ||
func JobWithJobInfo(info *JobInfo) OptionsFunc { | ||
return func(job *Job) { | ||
job.info = info | ||
} | ||
} | ||
|
||
// JobWithApiVersion set the ApiVersion of a Job | ||
func JobWithApiVersion(apiVersion string) OptionsFunc { | ||
return func(job *Job) { | ||
job.apiVersion = apiVersion | ||
} | ||
} | ||
|
||
// JobWithMapper adds a given ObjectMapper to the Job | ||
func JobWithMapper(mapper ObjectMapper) OptionsFunc { | ||
return func(job *Job) { | ||
job.objectMapper = mapper | ||
} | ||
} | ||
|
||
// JobWithOperation adds a given JobOperation to the Job | ||
func JobWithOperation(operation JobOperation) OptionsFunc { | ||
return func(job *Job) { | ||
job.operation = operation | ||
} | ||
} | ||
|
||
func (job *Job) Start() error { | ||
params := map[string]string{ | ||
"object": job.operation.Object, | ||
"operation": job.operation.Operation, | ||
} | ||
|
||
if err := job.forceApi.Post("/services/data/"+job.apiVersion+"/jobs/ingest", nil, params, job.info); err != nil { | ||
return err | ||
} | ||
job.operation.ProgressReporter("job created") | ||
return nil | ||
} | ||
|
||
// Run marshals the given payload to csv with the given ObjectMapper | ||
// for the Job and sends the csv to the given SalesforceJob | ||
func (job *Job) Run(payload any) error { | ||
if payload == nil { | ||
return errors.New("could not send payload because it is empty") | ||
} | ||
|
||
body, err := job.marshalCSV(payload) | ||
if err != nil { | ||
return fmt.Errorf("cannot marshal csv. %w", err) | ||
} | ||
|
||
contentUrl := job.info.ContentURL | ||
if !strings.HasPrefix(contentUrl, "/") { | ||
contentUrl = "/" + contentUrl | ||
} | ||
|
||
req, err := http.NewRequest("PUT", fmt.Sprintf("%s%s", job.forceApi.GetInstanceURL(), contentUrl), body) | ||
if err != nil { | ||
return fmt.Errorf("could not create new HTTP Request. %w", err) | ||
} | ||
|
||
req.Header.Set("Content-Type", "text/csv") | ||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", job.forceApi.GetAccessToken())) | ||
|
||
res, err := job.client.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("could not put csv bulk data. %w", err) | ||
} | ||
defer res.Body.Close() | ||
|
||
if res.StatusCode != http.StatusCreated { | ||
errb, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return fmt.Errorf("unexpected StatusCode on PUT batch: %d (%s), %s", res.StatusCode, res.Status, string(errb)) | ||
} | ||
|
||
statusURI := fmt.Sprintf("/services/data/%s/jobs/ingest/%s", job.apiVersion, job.info.Id) | ||
params := map[string]string{ | ||
"state": "UploadComplete", | ||
} | ||
|
||
if err := job.forceApi.Patch(statusURI, nil, params, job.info); err != nil { | ||
return err | ||
} | ||
|
||
job.operation.AddJobID(job.info.Id) | ||
|
||
return nil | ||
} | ||
|
||
func (job *Job) marshalCSV(payload any) (io.Reader, error) { | ||
// Map Objects to a csv Reader, for bulk api | ||
var bulkData bytes.Buffer | ||
w := csv.NewWriter(&bulkData) | ||
var records [][]string | ||
records = append(records, job.operation.Fields) | ||
records = append(records, job.objectMapper(payload)...) | ||
if err := w.WriteAll(records); err != nil { | ||
return nil, fmt.Errorf("could not create csv from records. %w", err) | ||
} | ||
return bytes.NewReader(bulkData.Bytes()), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package force | ||
|
||
import "io" | ||
|
||
type JobOperation struct { | ||
Operation string | ||
Object string | ||
Fields []string | ||
|
||
NumberRecordsFailed int | ||
NumberRecordsProcessed int | ||
ResponseMessages []string | ||
JobIDs []string | ||
WriteLine func(w io.Writer) bool | ||
ProgressReporter func(msg string) | ||
} | ||
|
||
func (op *JobOperation) AddJobID(id string) { | ||
op.JobIDs = append(op.JobIDs, id) | ||
} |
Oops, something went wrong.