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.
Files changed (80) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +74 -0
  3. package/bin/jsforce +3 -0
  4. package/bower.json +30 -0
  5. package/build/jsforce-api-analytics.js +393 -0
  6. package/build/jsforce-api-analytics.min.js +2 -0
  7. package/build/jsforce-api-analytics.min.js.map +1 -0
  8. package/build/jsforce-api-apex.js +183 -0
  9. package/build/jsforce-api-apex.min.js +2 -0
  10. package/build/jsforce-api-apex.min.js.map +1 -0
  11. package/build/jsforce-api-bulk.js +1054 -0
  12. package/build/jsforce-api-bulk.min.js +2 -0
  13. package/build/jsforce-api-bulk.min.js.map +1 -0
  14. package/build/jsforce-api-chatter.js +320 -0
  15. package/build/jsforce-api-chatter.min.js +2 -0
  16. package/build/jsforce-api-chatter.min.js.map +1 -0
  17. package/build/jsforce-api-metadata.js +3020 -0
  18. package/build/jsforce-api-metadata.min.js +2 -0
  19. package/build/jsforce-api-metadata.min.js.map +1 -0
  20. package/build/jsforce-api-soap.js +403 -0
  21. package/build/jsforce-api-soap.min.js +2 -0
  22. package/build/jsforce-api-soap.min.js.map +1 -0
  23. package/build/jsforce-api-streaming.js +3479 -0
  24. package/build/jsforce-api-streaming.min.js +2 -0
  25. package/build/jsforce-api-streaming.min.js.map +1 -0
  26. package/build/jsforce-api-tooling.js +319 -0
  27. package/build/jsforce-api-tooling.min.js +2 -0
  28. package/build/jsforce-api-tooling.min.js.map +1 -0
  29. package/build/jsforce-core.js +25250 -0
  30. package/build/jsforce-core.min.js +2 -0
  31. package/build/jsforce-core.min.js.map +1 -0
  32. package/build/jsforce.js +31637 -0
  33. package/build/jsforce.min.js +2 -0
  34. package/build/jsforce.min.js.map +1 -0
  35. package/core.js +1 -0
  36. package/index.js +1 -0
  37. package/lib/VERSION.js +2 -0
  38. package/lib/_required.js +29 -0
  39. package/lib/api/analytics.js +387 -0
  40. package/lib/api/apex.js +177 -0
  41. package/lib/api/bulk.js +862 -0
  42. package/lib/api/chatter.js +314 -0
  43. package/lib/api/index.js +8 -0
  44. package/lib/api/metadata.js +848 -0
  45. package/lib/api/soap.js +397 -0
  46. package/lib/api/streaming-extension.js +136 -0
  47. package/lib/api/streaming.js +270 -0
  48. package/lib/api/tooling.js +313 -0
  49. package/lib/browser/canvas.js +90 -0
  50. package/lib/browser/client.js +241 -0
  51. package/lib/browser/core.js +5 -0
  52. package/lib/browser/jsforce.js +6 -0
  53. package/lib/browser/jsonp.js +52 -0
  54. package/lib/browser/request.js +70 -0
  55. package/lib/cache.js +252 -0
  56. package/lib/cli/cli.js +431 -0
  57. package/lib/cli/repl.js +337 -0
  58. package/lib/connection.js +1881 -0
  59. package/lib/core.js +16 -0
  60. package/lib/csv.js +50 -0
  61. package/lib/date.js +163 -0
  62. package/lib/http-api.js +300 -0
  63. package/lib/jsforce.js +10 -0
  64. package/lib/logger.js +52 -0
  65. package/lib/oauth2.js +206 -0
  66. package/lib/process.js +275 -0
  67. package/lib/promise.js +164 -0
  68. package/lib/query.js +881 -0
  69. package/lib/quick-action.js +90 -0
  70. package/lib/record-stream.js +305 -0
  71. package/lib/record.js +107 -0
  72. package/lib/registry/file-registry.js +48 -0
  73. package/lib/registry/index.js +3 -0
  74. package/lib/registry/registry.js +111 -0
  75. package/lib/require.js +14 -0
  76. package/lib/soap.js +207 -0
  77. package/lib/sobject.js +558 -0
  78. package/lib/soql-builder.js +236 -0
  79. package/lib/transport.js +233 -0
  80. package/package.json +110 -0
@@ -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;