jsforce2 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +74 -0
- package/bin/jsforce +3 -0
- package/bower.json +30 -0
- package/build/jsforce-api-analytics.js +393 -0
- package/build/jsforce-api-analytics.min.js +2 -0
- package/build/jsforce-api-analytics.min.js.map +1 -0
- package/build/jsforce-api-apex.js +183 -0
- package/build/jsforce-api-apex.min.js +2 -0
- package/build/jsforce-api-apex.min.js.map +1 -0
- package/build/jsforce-api-bulk.js +1054 -0
- package/build/jsforce-api-bulk.min.js +2 -0
- package/build/jsforce-api-bulk.min.js.map +1 -0
- package/build/jsforce-api-chatter.js +320 -0
- package/build/jsforce-api-chatter.min.js +2 -0
- package/build/jsforce-api-chatter.min.js.map +1 -0
- package/build/jsforce-api-metadata.js +3020 -0
- package/build/jsforce-api-metadata.min.js +2 -0
- package/build/jsforce-api-metadata.min.js.map +1 -0
- package/build/jsforce-api-soap.js +403 -0
- package/build/jsforce-api-soap.min.js +2 -0
- package/build/jsforce-api-soap.min.js.map +1 -0
- package/build/jsforce-api-streaming.js +3479 -0
- package/build/jsforce-api-streaming.min.js +2 -0
- package/build/jsforce-api-streaming.min.js.map +1 -0
- package/build/jsforce-api-tooling.js +319 -0
- package/build/jsforce-api-tooling.min.js +2 -0
- package/build/jsforce-api-tooling.min.js.map +1 -0
- package/build/jsforce-core.js +25250 -0
- package/build/jsforce-core.min.js +2 -0
- package/build/jsforce-core.min.js.map +1 -0
- package/build/jsforce.js +31637 -0
- package/build/jsforce.min.js +2 -0
- package/build/jsforce.min.js.map +1 -0
- package/core.js +1 -0
- package/index.js +1 -0
- package/lib/VERSION.js +2 -0
- package/lib/_required.js +29 -0
- package/lib/api/analytics.js +387 -0
- package/lib/api/apex.js +177 -0
- package/lib/api/bulk.js +862 -0
- package/lib/api/chatter.js +314 -0
- package/lib/api/index.js +8 -0
- package/lib/api/metadata.js +848 -0
- package/lib/api/soap.js +397 -0
- package/lib/api/streaming-extension.js +136 -0
- package/lib/api/streaming.js +270 -0
- package/lib/api/tooling.js +313 -0
- package/lib/browser/canvas.js +90 -0
- package/lib/browser/client.js +241 -0
- package/lib/browser/core.js +5 -0
- package/lib/browser/jsforce.js +6 -0
- package/lib/browser/jsonp.js +52 -0
- package/lib/browser/request.js +70 -0
- package/lib/cache.js +252 -0
- package/lib/cli/cli.js +431 -0
- package/lib/cli/repl.js +337 -0
- package/lib/connection.js +1881 -0
- package/lib/core.js +16 -0
- package/lib/csv.js +50 -0
- package/lib/date.js +163 -0
- package/lib/http-api.js +300 -0
- package/lib/jsforce.js +10 -0
- package/lib/logger.js +52 -0
- package/lib/oauth2.js +206 -0
- package/lib/process.js +275 -0
- package/lib/promise.js +164 -0
- package/lib/query.js +881 -0
- package/lib/quick-action.js +90 -0
- package/lib/record-stream.js +305 -0
- package/lib/record.js +107 -0
- package/lib/registry/file-registry.js +48 -0
- package/lib/registry/index.js +3 -0
- package/lib/registry/registry.js +111 -0
- package/lib/require.js +14 -0
- package/lib/soap.js +207 -0
- package/lib/sobject.js +558 -0
- package/lib/soql-builder.js +236 -0
- package/lib/transport.js +233 -0
- package/package.json +110 -0
package/lib/api/bulk.js
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
/*global process*/
|
|
2
|
+
/**
|
|
3
|
+
* @file Manages Salesforce Bulk API related operations
|
|
4
|
+
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var inherits = require('inherits'),
|
|
10
|
+
stream = require('readable-stream'),
|
|
11
|
+
Duplex = stream.Duplex,
|
|
12
|
+
events = require('events'),
|
|
13
|
+
_ = require('lodash/core'),
|
|
14
|
+
joinStreams = require('multistream'),
|
|
15
|
+
jsforce = require('../core'),
|
|
16
|
+
RecordStream = require('../record-stream'),
|
|
17
|
+
Promise = require('../promise'),
|
|
18
|
+
HttpApi = require('../http-api');
|
|
19
|
+
|
|
20
|
+
/*--------------------------------------------*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Class for Bulk API Job
|
|
24
|
+
*
|
|
25
|
+
* @protected
|
|
26
|
+
* @class Bulk~Job
|
|
27
|
+
* @extends events.EventEmitter
|
|
28
|
+
*
|
|
29
|
+
* @param {Bulk} bulk - Bulk API object
|
|
30
|
+
* @param {String} [type] - SObject type
|
|
31
|
+
* @param {String} [operation] - Bulk load operation ('insert', 'update', 'upsert', 'delete', or 'hardDelete')
|
|
32
|
+
* @param {Object} [options] - Options for bulk loading operation
|
|
33
|
+
* @param {String} [options.extIdField] - External ID field name (used when upsert operation).
|
|
34
|
+
* @param {String} [options.concurrencyMode] - 'Serial' or 'Parallel'. Defaults to Parallel.
|
|
35
|
+
* @param {String} [jobId] - Job ID (if already available)
|
|
36
|
+
*/
|
|
37
|
+
var Job = function(bulk, type, operation, options, jobId) {
|
|
38
|
+
this._bulk = bulk;
|
|
39
|
+
this.type = type;
|
|
40
|
+
this.operation = operation;
|
|
41
|
+
this.options = options || {};
|
|
42
|
+
this.id = jobId;
|
|
43
|
+
this.state = this.id ? 'Open' : 'Unknown';
|
|
44
|
+
this._batches = {};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
inherits(Job, events.EventEmitter);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} Bulk~JobInfo
|
|
51
|
+
* @prop {String} id - Job ID
|
|
52
|
+
* @prop {String} object - Object type name
|
|
53
|
+
* @prop {String} operation - Operation type of the job
|
|
54
|
+
* @prop {String} state - Job status
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return latest jobInfo from cache
|
|
59
|
+
*
|
|
60
|
+
* @method Bulk~Job#info
|
|
61
|
+
* @param {Callback.<Bulk~JobInfo>} [callback] - Callback function
|
|
62
|
+
* @returns {Promise.<Bulk~JobInfo>}
|
|
63
|
+
*/
|
|
64
|
+
Job.prototype.info = function(callback) {
|
|
65
|
+
var self = this;
|
|
66
|
+
// if cache is not available, check the latest
|
|
67
|
+
if (!this._jobInfo) {
|
|
68
|
+
this._jobInfo = this.check();
|
|
69
|
+
}
|
|
70
|
+
return this._jobInfo.thenCall(callback);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Open new job and get jobinfo
|
|
75
|
+
*
|
|
76
|
+
* @method Bulk~Job#open
|
|
77
|
+
* @param {Callback.<Bulk~JobInfo>} [callback] - Callback function
|
|
78
|
+
* @returns {Promise.<Bulk~JobInfo>}
|
|
79
|
+
*/
|
|
80
|
+
Job.prototype.open = function(callback) {
|
|
81
|
+
var self = this;
|
|
82
|
+
var bulk = this._bulk;
|
|
83
|
+
var logger = bulk._logger;
|
|
84
|
+
|
|
85
|
+
// if not requested opening job
|
|
86
|
+
if (!this._jobInfo) {
|
|
87
|
+
var operation = this.operation.toLowerCase();
|
|
88
|
+
if (operation === 'harddelete') { operation = 'hardDelete'; }
|
|
89
|
+
var body = [
|
|
90
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
91
|
+
'<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">',
|
|
92
|
+
'<operation>' + operation + '</operation>',
|
|
93
|
+
'<object>' + this.type + '</object>',
|
|
94
|
+
(this.options.extIdField ?
|
|
95
|
+
'<externalIdFieldName>'+this.options.extIdField+'</externalIdFieldName>' :
|
|
96
|
+
''),
|
|
97
|
+
(this.options.concurrencyMode ?
|
|
98
|
+
'<concurrencyMode>'+this.options.concurrencyMode+'</concurrencyMode>' :
|
|
99
|
+
''),
|
|
100
|
+
(this.options.assignmentRuleId ?
|
|
101
|
+
'<assignmentRuleId>' + this.options.assignmentRuleId + '</assignmentRuleId>' :
|
|
102
|
+
''),
|
|
103
|
+
'<contentType>CSV</contentType>',
|
|
104
|
+
'</jobInfo>'
|
|
105
|
+
].join('');
|
|
106
|
+
|
|
107
|
+
this._jobInfo = bulk._request({
|
|
108
|
+
method : 'POST',
|
|
109
|
+
path : "/job",
|
|
110
|
+
body : body,
|
|
111
|
+
headers : {
|
|
112
|
+
"Content-Type" : "application/xml; charset=utf-8"
|
|
113
|
+
},
|
|
114
|
+
responseType: "application/xml"
|
|
115
|
+
}).then(function(res) {
|
|
116
|
+
self.emit("open", res.jobInfo);
|
|
117
|
+
self.id = res.jobInfo.id;
|
|
118
|
+
self.state = res.jobInfo.state;
|
|
119
|
+
return res.jobInfo;
|
|
120
|
+
}, function(err) {
|
|
121
|
+
self.emit("error", err);
|
|
122
|
+
throw err;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return this._jobInfo.thenCall(callback);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create a new batch instance in the job
|
|
130
|
+
*
|
|
131
|
+
* @method Bulk~Job#createBatch
|
|
132
|
+
* @param {String[]} [columns] - Columns
|
|
133
|
+
* @returns {Bulk~Batch}
|
|
134
|
+
*/
|
|
135
|
+
Job.prototype.createBatch = function(columns) {
|
|
136
|
+
var batch = new Batch(this, undefined, columns);
|
|
137
|
+
var self = this;
|
|
138
|
+
batch.on('queue', function() {
|
|
139
|
+
self._batches[batch.id] = batch;
|
|
140
|
+
});
|
|
141
|
+
return batch;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get a batch instance specified by given batch ID
|
|
146
|
+
*
|
|
147
|
+
* @method Bulk~Job#batch
|
|
148
|
+
* @param {String} batchId - Batch ID
|
|
149
|
+
* @returns {Bulk~Batch}
|
|
150
|
+
*/
|
|
151
|
+
Job.prototype.batch = function(batchId) {
|
|
152
|
+
var batch = this._batches[batchId];
|
|
153
|
+
if (!batch) {
|
|
154
|
+
batch = new Batch(this, batchId);
|
|
155
|
+
this._batches[batchId] = batch;
|
|
156
|
+
}
|
|
157
|
+
return batch;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check the latest job status from server
|
|
162
|
+
*
|
|
163
|
+
* @method Bulk~Job#check
|
|
164
|
+
* @param {Callback.<Bulk~JobInfo>} [callback] - Callback function
|
|
165
|
+
* @returns {Promise.<Bulk~JobInfo>}
|
|
166
|
+
*/
|
|
167
|
+
Job.prototype.check = function(callback) {
|
|
168
|
+
var self = this;
|
|
169
|
+
var bulk = this._bulk;
|
|
170
|
+
var logger = bulk._logger;
|
|
171
|
+
|
|
172
|
+
this._jobInfo = this._waitAssign().then(function() {
|
|
173
|
+
return bulk._request({
|
|
174
|
+
method : 'GET',
|
|
175
|
+
path : "/job/" + self.id,
|
|
176
|
+
responseType: "application/xml"
|
|
177
|
+
});
|
|
178
|
+
}).then(function(res) {
|
|
179
|
+
logger.debug(res.jobInfo);
|
|
180
|
+
self.id = res.jobInfo.id;
|
|
181
|
+
self.type = res.jobInfo.object;
|
|
182
|
+
self.operation = res.jobInfo.operation;
|
|
183
|
+
self.state = res.jobInfo.state;
|
|
184
|
+
return res.jobInfo;
|
|
185
|
+
});
|
|
186
|
+
return this._jobInfo.thenCall(callback);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Wait till the job is assigned to server
|
|
191
|
+
*
|
|
192
|
+
* @method Bulk~Job#info
|
|
193
|
+
* @param {Callback.<Bulk~JobInfo>} [callback] - Callback function
|
|
194
|
+
* @returns {Promise.<Bulk~JobInfo>}
|
|
195
|
+
*/
|
|
196
|
+
Job.prototype._waitAssign = function(callback) {
|
|
197
|
+
return (this.id ? Promise.resolve({ id: this.id }) : this.open()).thenCall(callback);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* List all registered batch info in job
|
|
203
|
+
*
|
|
204
|
+
* @method Bulk~Job#list
|
|
205
|
+
* @param {Callback.<Array.<Bulk~BatchInfo>>} [callback] - Callback function
|
|
206
|
+
* @returns {Promise.<Array.<Bulk~BatchInfo>>}
|
|
207
|
+
*/
|
|
208
|
+
Job.prototype.list = function(callback) {
|
|
209
|
+
var self = this;
|
|
210
|
+
var bulk = this._bulk;
|
|
211
|
+
var logger = bulk._logger;
|
|
212
|
+
|
|
213
|
+
return this._waitAssign().then(function() {
|
|
214
|
+
return bulk._request({
|
|
215
|
+
method : 'GET',
|
|
216
|
+
path : "/job/" + self.id + "/batch",
|
|
217
|
+
responseType: "application/xml"
|
|
218
|
+
});
|
|
219
|
+
}).then(function(res) {
|
|
220
|
+
logger.debug(res.batchInfoList.batchInfo);
|
|
221
|
+
var batchInfoList = res.batchInfoList;
|
|
222
|
+
batchInfoList = _.isArray(batchInfoList.batchInfo) ? batchInfoList.batchInfo : [ batchInfoList.batchInfo ];
|
|
223
|
+
return batchInfoList;
|
|
224
|
+
}).thenCall(callback);
|
|
225
|
+
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Close opened job
|
|
230
|
+
*
|
|
231
|
+
* @method Bulk~Job#close
|
|
232
|
+
* @param {Callback.<Bulk~JobInfo>} [callback] - Callback function
|
|
233
|
+
* @returns {Promise.<Bulk~JobInfo>}
|
|
234
|
+
*/
|
|
235
|
+
Job.prototype.close = function() {
|
|
236
|
+
var self = this;
|
|
237
|
+
return this._changeState("Closed").then(function(jobInfo) {
|
|
238
|
+
self.id = null;
|
|
239
|
+
self.emit("close", jobInfo);
|
|
240
|
+
return jobInfo;
|
|
241
|
+
}, function(err) {
|
|
242
|
+
self.emit("error", err);
|
|
243
|
+
throw err;
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Set the status to abort
|
|
249
|
+
*
|
|
250
|
+
* @method Bulk~Job#abort
|
|
251
|
+
* @param {Callback.<Bulk~JobInfo>} [callback] - Callback function
|
|
252
|
+
* @returns {Promise.<Bulk~JobInfo>}
|
|
253
|
+
*/
|
|
254
|
+
Job.prototype.abort = function() {
|
|
255
|
+
var self = this;
|
|
256
|
+
return this._changeState("Aborted").then(function(jobInfo) {
|
|
257
|
+
self.id = null;
|
|
258
|
+
self.emit("abort", jobInfo);
|
|
259
|
+
return jobInfo;
|
|
260
|
+
}, function(err) {
|
|
261
|
+
self.emit("error", err);
|
|
262
|
+
throw err;
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
Job.prototype._changeState = function(state, callback) {
|
|
270
|
+
var self = this;
|
|
271
|
+
var bulk = this._bulk;
|
|
272
|
+
var logger = bulk._logger;
|
|
273
|
+
|
|
274
|
+
this._jobInfo = this._waitAssign().then(function() {
|
|
275
|
+
var body = [
|
|
276
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
277
|
+
'<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">',
|
|
278
|
+
'<state>' + state + '</state>',
|
|
279
|
+
'</jobInfo>'
|
|
280
|
+
].join('');
|
|
281
|
+
return bulk._request({
|
|
282
|
+
method : 'POST',
|
|
283
|
+
path : "/job/" + self.id,
|
|
284
|
+
body : body,
|
|
285
|
+
headers : {
|
|
286
|
+
"Content-Type" : "application/xml; charset=utf-8"
|
|
287
|
+
},
|
|
288
|
+
responseType: "application/xml"
|
|
289
|
+
});
|
|
290
|
+
}).then(function(res) {
|
|
291
|
+
logger.debug(res.jobInfo);
|
|
292
|
+
self.state = res.jobInfo.state;
|
|
293
|
+
return res.jobInfo;
|
|
294
|
+
});
|
|
295
|
+
return this._jobInfo.thenCall(callback);
|
|
296
|
+
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
/*--------------------------------------------*/
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Batch (extends RecordStream)
|
|
304
|
+
*
|
|
305
|
+
* @protected
|
|
306
|
+
* @class Bulk~Batch
|
|
307
|
+
* @extends {stream.Writable}
|
|
308
|
+
* @implements {Promise.<Array.<RecordResult>>}
|
|
309
|
+
* @param {Bulk~Job} job - Bulk job object
|
|
310
|
+
* @param {String} [batchId] - Batch ID (if already available)
|
|
311
|
+
* @param {String[]} [columns] - Columns
|
|
312
|
+
*/
|
|
313
|
+
var Batch = function(job, batchId, columns) {
|
|
314
|
+
Batch.super_.call(this, { objectMode: true });
|
|
315
|
+
this.job = job;
|
|
316
|
+
this.id = batchId;
|
|
317
|
+
this._bulk = job._bulk;
|
|
318
|
+
this._deferred = Promise.defer();
|
|
319
|
+
this._setupDataStreams(columns);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
inherits(Batch, stream.Writable);
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @private
|
|
327
|
+
*/
|
|
328
|
+
Batch.prototype._setupDataStreams = function(columns) {
|
|
329
|
+
var batch = this;
|
|
330
|
+
var converterOptions = { nullValue : '#N/A' };
|
|
331
|
+
this._uploadStream = new RecordStream.Serializable();
|
|
332
|
+
this._uploadDataStream = this._uploadStream.stream('csv', { ...converterOptions, columns });
|
|
333
|
+
this._downloadStream = new RecordStream.Parsable();
|
|
334
|
+
this._downloadDataStream = this._downloadStream.stream('csv', converterOptions);
|
|
335
|
+
|
|
336
|
+
this.on('finish', function() {
|
|
337
|
+
batch._uploadStream.end();
|
|
338
|
+
});
|
|
339
|
+
this._uploadDataStream.once('readable', function() {
|
|
340
|
+
batch.job.open().then(function() {
|
|
341
|
+
// pipe upload data to batch API request stream
|
|
342
|
+
batch._uploadDataStream.pipe(batch._createRequestStream());
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// duplex data stream, opened access to API programmers by Batch#stream()
|
|
347
|
+
var dataStream = this._dataStream = new Duplex();
|
|
348
|
+
dataStream._write = function(data, enc, cb) {
|
|
349
|
+
batch._uploadDataStream.write(data, enc, cb);
|
|
350
|
+
};
|
|
351
|
+
dataStream.on('finish', function() {
|
|
352
|
+
batch._uploadDataStream.end();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
this._downloadDataStream.on('readable', function() {
|
|
356
|
+
dataStream.read(0);
|
|
357
|
+
});
|
|
358
|
+
this._downloadDataStream.on('end', function() {
|
|
359
|
+
dataStream.push(null);
|
|
360
|
+
});
|
|
361
|
+
dataStream._read = function(size) {
|
|
362
|
+
var chunk;
|
|
363
|
+
while ((chunk = batch._downloadDataStream.read()) !== null) {
|
|
364
|
+
dataStream.push(chunk);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Connect batch API and create stream instance of request/response
|
|
371
|
+
*
|
|
372
|
+
* @private
|
|
373
|
+
* @returns {stream.Duplex}
|
|
374
|
+
*/
|
|
375
|
+
Batch.prototype._createRequestStream = function() {
|
|
376
|
+
var batch = this;
|
|
377
|
+
var bulk = batch._bulk;
|
|
378
|
+
var logger = bulk._logger;
|
|
379
|
+
|
|
380
|
+
return bulk._request({
|
|
381
|
+
method : 'POST',
|
|
382
|
+
path : "/job/" + batch.job.id + "/batch",
|
|
383
|
+
headers: {
|
|
384
|
+
"Content-Type": "text/csv"
|
|
385
|
+
},
|
|
386
|
+
responseType: "application/xml"
|
|
387
|
+
}, function(err, res) {
|
|
388
|
+
if (err) {
|
|
389
|
+
batch.emit('error', err);
|
|
390
|
+
} else {
|
|
391
|
+
logger.debug(res.batchInfo);
|
|
392
|
+
batch.id = res.batchInfo.id;
|
|
393
|
+
batch.emit('queue', res.batchInfo);
|
|
394
|
+
}
|
|
395
|
+
}).stream();
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Implementation of Writable
|
|
400
|
+
*
|
|
401
|
+
* @override
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
Batch.prototype._write = function(record, enc, cb) {
|
|
405
|
+
record = _.clone(record);
|
|
406
|
+
if (this.job.operation === "insert") {
|
|
407
|
+
delete record.Id;
|
|
408
|
+
} else if (this.job.operation === "delete") {
|
|
409
|
+
record = { Id: record.Id };
|
|
410
|
+
}
|
|
411
|
+
delete record.type;
|
|
412
|
+
delete record.attributes;
|
|
413
|
+
this._uploadStream.write(record, enc, cb);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Returns duplex stream which accepts CSV data input and batch result output
|
|
418
|
+
*
|
|
419
|
+
* @returns {stream.Duplex}
|
|
420
|
+
*/
|
|
421
|
+
Batch.prototype.stream = function() {
|
|
422
|
+
return this._dataStream;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Execute batch operation
|
|
427
|
+
*
|
|
428
|
+
* @method Bulk~Batch#execute
|
|
429
|
+
* @param {Array.<Record>|stream.Stream|String} [input] - Input source for batch operation. Accepts array of records, CSV string, and CSV data input stream in insert/update/upsert/delete/hardDelete operation, SOQL string in query operation.
|
|
430
|
+
* @param {Callback.<Array.<RecordResult>|Array.<BatchResultInfo>>} [callback] - Callback function
|
|
431
|
+
* @returns {Bulk~Batch}
|
|
432
|
+
*/
|
|
433
|
+
Batch.prototype.run =
|
|
434
|
+
Batch.prototype.exec =
|
|
435
|
+
Batch.prototype.execute = function(input, callback) {
|
|
436
|
+
var self = this;
|
|
437
|
+
|
|
438
|
+
if (typeof input === 'function') { // if input argument is omitted
|
|
439
|
+
callback = input;
|
|
440
|
+
input = null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// if batch is already executed
|
|
444
|
+
if (this._result) {
|
|
445
|
+
throw new Error("Batch already executed.");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
var rdeferred = Promise.defer();
|
|
449
|
+
this._result = rdeferred.promise;
|
|
450
|
+
this._result.then(function(res) {
|
|
451
|
+
self._deferred.resolve(res);
|
|
452
|
+
}, function(err) {
|
|
453
|
+
self._deferred.reject(err);
|
|
454
|
+
});
|
|
455
|
+
this.once('response', function(res) {
|
|
456
|
+
rdeferred.resolve(res);
|
|
457
|
+
});
|
|
458
|
+
this.once('error', function(err) {
|
|
459
|
+
rdeferred.reject(err);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (_.isObject(input) && _.isFunction(input.pipe)) { // if input has stream.Readable interface
|
|
463
|
+
input.pipe(this._dataStream);
|
|
464
|
+
} else {
|
|
465
|
+
var data;
|
|
466
|
+
if (_.isArray(input)) {
|
|
467
|
+
_.forEach(input, function(record) {
|
|
468
|
+
Object.keys(record).forEach(function(key) {
|
|
469
|
+
if (typeof record[key] === 'boolean') {
|
|
470
|
+
record[key] = String(record[key])
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
self.write(record);
|
|
474
|
+
});
|
|
475
|
+
self.end();
|
|
476
|
+
} else if (_.isString(input)){
|
|
477
|
+
data = input;
|
|
478
|
+
this._dataStream.write(data, 'utf8');
|
|
479
|
+
this._dataStream.end();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// return Batch instance for chaining
|
|
484
|
+
return this.thenCall(callback);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Promise/A+ interface
|
|
489
|
+
* http://promises-aplus.github.io/promises-spec/
|
|
490
|
+
*
|
|
491
|
+
* Delegate to deferred promise, return promise instance for batch result
|
|
492
|
+
*
|
|
493
|
+
* @method Bulk~Batch#then
|
|
494
|
+
*/
|
|
495
|
+
Batch.prototype.then = function(onResolved, onReject, onProgress) {
|
|
496
|
+
return this._deferred.promise.then(onResolved, onReject, onProgress);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Promise/A+ extension
|
|
501
|
+
* Call "then" using given node-style callback function
|
|
502
|
+
*
|
|
503
|
+
* @method Bulk~Batch#thenCall
|
|
504
|
+
*/
|
|
505
|
+
Batch.prototype.thenCall = function(callback) {
|
|
506
|
+
if (_.isFunction(callback)) {
|
|
507
|
+
this.then(function(res) {
|
|
508
|
+
process.nextTick(function() {
|
|
509
|
+
callback(null, res);
|
|
510
|
+
});
|
|
511
|
+
}, function(err) {
|
|
512
|
+
process.nextTick(function() {
|
|
513
|
+
callback(err);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return this;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @typedef {Object} Bulk~BatchInfo
|
|
522
|
+
* @prop {String} id - Batch ID
|
|
523
|
+
* @prop {String} jobId - Job ID
|
|
524
|
+
* @prop {String} state - Batch state
|
|
525
|
+
* @prop {String} stateMessage - Batch state message
|
|
526
|
+
*/
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Check the latest batch status in server
|
|
530
|
+
*
|
|
531
|
+
* @method Bulk~Batch#check
|
|
532
|
+
* @param {Callback.<Bulk~BatchInfo>} [callback] - Callback function
|
|
533
|
+
* @returns {Promise.<Bulk~BatchInfo>}
|
|
534
|
+
*/
|
|
535
|
+
Batch.prototype.check = function(callback) {
|
|
536
|
+
var self = this;
|
|
537
|
+
var bulk = this._bulk;
|
|
538
|
+
var logger = bulk._logger;
|
|
539
|
+
var jobId = this.job.id;
|
|
540
|
+
var batchId = this.id;
|
|
541
|
+
|
|
542
|
+
if (!jobId || !batchId) {
|
|
543
|
+
callback("Batch not started", null);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
return bulk._request({
|
|
547
|
+
method : 'GET',
|
|
548
|
+
path : "/job/" + jobId + "/batch/" + batchId,
|
|
549
|
+
responseType: "application/xml"
|
|
550
|
+
}).then(function(res) {
|
|
551
|
+
logger.debug(res.batchInfo);
|
|
552
|
+
return res.batchInfo;
|
|
553
|
+
}).thenCall(callback);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Polling the batch result and retrieve
|
|
559
|
+
*
|
|
560
|
+
* @method Bulk~Batch#poll
|
|
561
|
+
* @param {Number} interval - Polling interval in milliseconds
|
|
562
|
+
* @param {Number} timeout - Polling timeout in milliseconds
|
|
563
|
+
*/
|
|
564
|
+
Batch.prototype.poll = function(interval, timeout) {
|
|
565
|
+
var self = this;
|
|
566
|
+
var jobId = this.job.id;
|
|
567
|
+
var batchId = this.id;
|
|
568
|
+
|
|
569
|
+
if (!jobId || !batchId) {
|
|
570
|
+
throw new Error("Batch not started.");
|
|
571
|
+
}
|
|
572
|
+
var startTime = new Date().getTime();
|
|
573
|
+
var poll = function() {
|
|
574
|
+
var now = new Date().getTime();
|
|
575
|
+
if (startTime + timeout < now) {
|
|
576
|
+
var err = new Error("Polling time out. Job Id = " + jobId + " , batch Id = " + batchId);
|
|
577
|
+
err.name = 'PollingTimeout';
|
|
578
|
+
err.jobId = jobId;
|
|
579
|
+
err.batchId = batchId;
|
|
580
|
+
self.emit('error', err);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
self.check(function(err, res) {
|
|
584
|
+
if (err) {
|
|
585
|
+
self.emit('error', err);
|
|
586
|
+
} else {
|
|
587
|
+
if (res.state === "Failed") {
|
|
588
|
+
if (parseInt(res.numberRecordsProcessed, 10) > 0) {
|
|
589
|
+
self.retrieve();
|
|
590
|
+
} else {
|
|
591
|
+
self.emit('error', new Error(res.stateMessage));
|
|
592
|
+
}
|
|
593
|
+
} else if (res.state === "Completed") {
|
|
594
|
+
self.retrieve();
|
|
595
|
+
} else {
|
|
596
|
+
self.emit('progress', res);
|
|
597
|
+
setTimeout(poll, interval);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
setTimeout(poll, interval);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* @typedef {Object} Bulk~BatchResultInfo
|
|
607
|
+
* @prop {String} id - Batch result ID
|
|
608
|
+
* @prop {String} batchId - Batch ID which includes this batch result.
|
|
609
|
+
* @prop {String} jobId - Job ID which includes this batch result.
|
|
610
|
+
*/
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Retrieve batch result
|
|
614
|
+
*
|
|
615
|
+
* @method Bulk~Batch#retrieve
|
|
616
|
+
* @param {Callback.<Array.<RecordResult>|Array.<Bulk~BatchResultInfo>>} [callback] - Callback function
|
|
617
|
+
* @returns {Promise.<Array.<RecordResult>|Array.<Bulk~BatchResultInfo>>}
|
|
618
|
+
*/
|
|
619
|
+
Batch.prototype.retrieve = function(callback) {
|
|
620
|
+
var self = this;
|
|
621
|
+
var bulk = this._bulk;
|
|
622
|
+
var jobId = this.job.id;
|
|
623
|
+
var job = this.job;
|
|
624
|
+
var batchId = this.id;
|
|
625
|
+
|
|
626
|
+
if (!jobId || !batchId) {
|
|
627
|
+
throw new Error("Batch not started.");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return job.info().then(function(jobInfo) {
|
|
631
|
+
return bulk._request({
|
|
632
|
+
method : 'GET',
|
|
633
|
+
path : "/job/" + jobId + "/batch/" + batchId + "/result"
|
|
634
|
+
});
|
|
635
|
+
}).then(function(res) {
|
|
636
|
+
var results;
|
|
637
|
+
if (job.operation === 'query') {
|
|
638
|
+
var conn = bulk._conn;
|
|
639
|
+
var resultIds = res['result-list'].result;
|
|
640
|
+
results = res['result-list'].result;
|
|
641
|
+
results = _.map(_.isArray(results) ? results : [ results ], function(id) {
|
|
642
|
+
return {
|
|
643
|
+
id: id,
|
|
644
|
+
batchId: batchId,
|
|
645
|
+
jobId: jobId
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
results = _.map(res, function(ret) {
|
|
650
|
+
return {
|
|
651
|
+
id: ret.Id || null,
|
|
652
|
+
success: ret.Success === "true",
|
|
653
|
+
errors: ret.Error ? [ ret.Error ] : []
|
|
654
|
+
};
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
self.emit('response', results);
|
|
658
|
+
return results;
|
|
659
|
+
}).fail(function(err) {
|
|
660
|
+
self.emit('error', err);
|
|
661
|
+
throw err;
|
|
662
|
+
}).thenCall(callback);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Fetch query result as a record stream
|
|
667
|
+
* @param {String} resultId - Result id
|
|
668
|
+
* @returns {RecordStream} - Record stream, convertible to CSV data stream
|
|
669
|
+
*/
|
|
670
|
+
Batch.prototype.result = function(resultId) {
|
|
671
|
+
var jobId = this.job.id;
|
|
672
|
+
var batchId = this.id;
|
|
673
|
+
if (!jobId || !batchId) {
|
|
674
|
+
throw new Error("Batch not started.");
|
|
675
|
+
}
|
|
676
|
+
var resultStream = new RecordStream.Parsable();
|
|
677
|
+
var resultDataStream = resultStream.stream('csv');
|
|
678
|
+
var reqStream = this._bulk._request({
|
|
679
|
+
method : 'GET',
|
|
680
|
+
path : "/job/" + jobId + "/batch/" + batchId + "/result/" + resultId,
|
|
681
|
+
responseType: "application/octet-stream"
|
|
682
|
+
}).stream().pipe(resultDataStream);
|
|
683
|
+
return resultStream;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
/*--------------------------------------------*/
|
|
687
|
+
/**
|
|
688
|
+
* @private
|
|
689
|
+
*/
|
|
690
|
+
var BulkApi = function() {
|
|
691
|
+
BulkApi.super_.apply(this, arguments);
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
inherits(BulkApi, HttpApi);
|
|
695
|
+
|
|
696
|
+
BulkApi.prototype.beforeSend = function(request) {
|
|
697
|
+
request.headers = request.headers || {};
|
|
698
|
+
request.headers["X-SFDC-SESSION"] = this._conn.accessToken;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
BulkApi.prototype.isSessionExpired = function(response) {
|
|
702
|
+
return response.statusCode === 400 &&
|
|
703
|
+
/<exceptionCode>InvalidSessionId<\/exceptionCode>/.test(response.body);
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
BulkApi.prototype.hasErrorInResponseBody = function(body) {
|
|
707
|
+
return !!body.error;
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
BulkApi.prototype.parseError = function(body) {
|
|
711
|
+
return {
|
|
712
|
+
errorCode: body.error.exceptionCode,
|
|
713
|
+
message: body.error.exceptionMessage
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
/*--------------------------------------------*/
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Class for Bulk API
|
|
721
|
+
*
|
|
722
|
+
* @class
|
|
723
|
+
* @param {Connection} conn - Connection object
|
|
724
|
+
*/
|
|
725
|
+
var Bulk = function(conn) {
|
|
726
|
+
this._conn = conn;
|
|
727
|
+
this._logger = conn._logger;
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Polling interval in milliseconds
|
|
732
|
+
* @type {Number}
|
|
733
|
+
*/
|
|
734
|
+
Bulk.prototype.pollInterval = 1000;
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Polling timeout in milliseconds
|
|
738
|
+
* @type {Number}
|
|
739
|
+
*/
|
|
740
|
+
Bulk.prototype.pollTimeout = 10000;
|
|
741
|
+
|
|
742
|
+
/** @private **/
|
|
743
|
+
Bulk.prototype._request = function(request, callback) {
|
|
744
|
+
var conn = this._conn;
|
|
745
|
+
request = _.clone(request);
|
|
746
|
+
var baseUrl = [ conn.instanceUrl, "services/async", conn.version ].join('/');
|
|
747
|
+
request.url = baseUrl + request.path;
|
|
748
|
+
var options = { responseType: request.responseType };
|
|
749
|
+
delete request.path;
|
|
750
|
+
delete request.responseType;
|
|
751
|
+
return new BulkApi(this._conn, options).request(request).thenCall(callback);
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Create and start bulkload job and batch
|
|
756
|
+
*
|
|
757
|
+
* @param {String} type - SObject type
|
|
758
|
+
* @param {String} operation - Bulk load operation ('insert', 'update', 'upsert', 'delete', or 'hardDelete')
|
|
759
|
+
* @param {Object} [options] - Options for bulk loading operation
|
|
760
|
+
* @param {String} [options.extIdField] - External ID field name (used when upsert operation).
|
|
761
|
+
* @param {String} [options.concurrencyMode] - 'Serial' or 'Parallel'. Defaults to Parallel.
|
|
762
|
+
* @param {Array.<Record>|stream.Stream|String} [input] - Input source for bulkload. Accepts array of records, CSV string, and CSV data input stream in insert/update/upsert/delete/hardDelete operation, SOQL string in query operation.
|
|
763
|
+
* @param {Callback.<Array.<RecordResult>|Array.<Bulk~BatchResultInfo>>} [callback] - Callback function
|
|
764
|
+
* @returns {Bulk~Batch}
|
|
765
|
+
*/
|
|
766
|
+
Bulk.prototype.load = function(type, operation, options, input, callback) {
|
|
767
|
+
var self = this;
|
|
768
|
+
if (!type || !operation) {
|
|
769
|
+
throw new Error("Insufficient arguments. At least, 'type' and 'operation' are required.");
|
|
770
|
+
}
|
|
771
|
+
if (!_.isObject(options) || options.constructor !== Object) { // when options is not plain hash object, it is omitted
|
|
772
|
+
callback = input;
|
|
773
|
+
input = options;
|
|
774
|
+
options = null;
|
|
775
|
+
}
|
|
776
|
+
var job = this.createJob(type, operation, options);
|
|
777
|
+
job.once('error', function (error) {
|
|
778
|
+
if (batch) {
|
|
779
|
+
batch.emit('error', error); // pass job error to batch
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
var batch = job.createBatch();
|
|
783
|
+
var cleanup = function() {
|
|
784
|
+
batch = null;
|
|
785
|
+
job.close();
|
|
786
|
+
};
|
|
787
|
+
var cleanupOnError = function(err) {
|
|
788
|
+
if (err.name !== 'PollingTimeout') {
|
|
789
|
+
cleanup();
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
batch.on('response', cleanup);
|
|
793
|
+
batch.on('error', cleanupOnError);
|
|
794
|
+
batch.on('queue', function() { batch.poll(self.pollInterval, self.pollTimeout); });
|
|
795
|
+
return batch.execute(input, callback);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Execute bulk query and get record stream
|
|
800
|
+
*
|
|
801
|
+
* @param {String} soql - SOQL to execute in bulk job
|
|
802
|
+
* @returns {RecordStream.Parsable} - Record stream, convertible to CSV data stream
|
|
803
|
+
*/
|
|
804
|
+
Bulk.prototype.query = function(soql) {
|
|
805
|
+
var m = soql.replace(/\([\s\S]+\)/g, '').match(/FROM\s+(\w+)/i);
|
|
806
|
+
if (!m) {
|
|
807
|
+
throw new Error("No sobject type found in query, maybe caused by invalid SOQL.");
|
|
808
|
+
}
|
|
809
|
+
var type = m[1];
|
|
810
|
+
var self = this;
|
|
811
|
+
var recordStream = new RecordStream.Parsable();
|
|
812
|
+
var dataStream = recordStream.stream('csv');
|
|
813
|
+
this.load(type, "query", soql).then(function(results) {
|
|
814
|
+
var streams = results.map(function(result) {
|
|
815
|
+
return self
|
|
816
|
+
.job(result.jobId)
|
|
817
|
+
.batch(result.batchId)
|
|
818
|
+
.result(result.id)
|
|
819
|
+
.stream();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
joinStreams(streams).pipe(dataStream);
|
|
823
|
+
}).fail(function(err) {
|
|
824
|
+
recordStream.emit('error', err);
|
|
825
|
+
});
|
|
826
|
+
return recordStream;
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Create a new job instance
|
|
832
|
+
*
|
|
833
|
+
* @param {String} type - SObject type
|
|
834
|
+
* @param {String} operation - Bulk load operation ('insert', 'update', 'upsert', 'delete', 'hardDelete', or 'query')
|
|
835
|
+
* @param {Object} [options] - Options for bulk loading operation
|
|
836
|
+
* @returns {Bulk~Job}
|
|
837
|
+
*/
|
|
838
|
+
Bulk.prototype.createJob = function(type, operation, options) {
|
|
839
|
+
return new Job(this, type, operation, options);
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Get a job instance specified by given job ID
|
|
844
|
+
*
|
|
845
|
+
* @param {String} jobId - Job ID
|
|
846
|
+
* @returns {Bulk~Job}
|
|
847
|
+
*/
|
|
848
|
+
Bulk.prototype.job = function(jobId) {
|
|
849
|
+
return new Job(this, null, null, null, jobId);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
/*--------------------------------------------*/
|
|
854
|
+
/*
|
|
855
|
+
* Register hook in connection instantiation for dynamically adding this API module features
|
|
856
|
+
*/
|
|
857
|
+
jsforce.on('connection:new', function(conn) {
|
|
858
|
+
conn.bulk = new Bulk(conn);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
module.exports = Bulk;
|