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
package/lib/query.js ADDED
@@ -0,0 +1,881 @@
1
+ /*global process*/
2
+ /**
3
+ * @file Manages query for records in Salesforce
4
+ * @author Shinichi Tomita <shinichi.tomita@gmail.com>
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ var inherits = require('inherits'),
10
+ events = require('events'),
11
+ stream = require('readable-stream'),
12
+ _ = require('lodash/core'),
13
+ Promise = require('./promise'),
14
+ SfDate = require("./date"),
15
+ SOQLBuilder = require("./soql-builder"),
16
+ RecordStream = require("./record-stream");
17
+
18
+ /**
19
+ * Query
20
+ *
21
+ * @protected
22
+ * @class
23
+ * @extends {stream.Readable}
24
+ * @implements Promise.<T>
25
+ * @template T
26
+ * @param {Connection} conn - Connection object
27
+ * @param {Object|String} config - Query config object or SOQL string
28
+ * @param {Object} [options] - Default query options
29
+ * @param {Boolean} [options.autoFetch] - Using auto fetch mode or not
30
+ * @param {Number} [options.maxFetch] - Max fetching records in auto fetch mode
31
+ * @param {Boolean} [options.scanAll] - Including deleted records for query target or not
32
+ * @param {Object} [options.headers] - Additional HTTP request headers sent in query request
33
+ */
34
+ var Query = module.exports = function(conn, config, options) {
35
+ Query.super_.call(this, { objectMode: true });
36
+
37
+ this._conn = conn;
38
+ if (_.isString(config)) { // if query config is string, it is given in SOQL.
39
+ this._soql = config;
40
+ } else if (config.locator && config.locator.indexOf("/") >= 0) { // if locator given in url for next records
41
+ this._locator = config.locator.split("/").pop();
42
+ } else {
43
+ this._config = config;
44
+ this.select(config.fields);
45
+ if (config.includes) {
46
+ this.include(config.includes);
47
+ }
48
+ if (config.sort) {
49
+ this.sort(config.sort);
50
+ }
51
+ }
52
+ this._options = _.defaults(options || {}, {
53
+ maxFetch: 10000,
54
+ autoFetch: false,
55
+ scanAll: false,
56
+ responseTarget: ResponseTargets.QueryResult
57
+ });
58
+ this._executed = false;
59
+ this._finished = false;
60
+ this._chaining = false;
61
+
62
+ this._deferred = Promise.defer();
63
+
64
+ var self = this;
65
+ };
66
+
67
+ inherits(Query, stream.Readable);
68
+
69
+ /**
70
+ * Select fields to include in the returning result
71
+ *
72
+ * @param {Object|Array.<String>|String} fields - Fields to fetch. Format can be in JSON object (MongoDB-like), array of field names, or comma-separated field names.
73
+ * @returns {Query.<T>}
74
+ */
75
+ Query.prototype.select = function(fields) {
76
+ if (this._soql) {
77
+ throw Error("Cannot set select fields for the query which has already built SOQL.");
78
+ }
79
+ fields = fields || '*';
80
+ if (_.isString(fields)) {
81
+ fields = fields.split(/\s*,\s*/);
82
+ } else if (_.isObject(fields) && !_.isArray(fields)) {
83
+ var _fields = [];
84
+ for (var k in fields) {
85
+ if (fields[k]) { _fields.push(k); }
86
+ }
87
+ fields = _fields;
88
+ }
89
+ this._config.fields = fields;
90
+ return this;
91
+ };
92
+
93
+ /**
94
+ * Set query conditions to filter the result records
95
+ *
96
+ * @param {Object|String} conditions - Conditions in JSON object (MongoDB-like), or raw SOQL WHERE clause string.
97
+ * @returns {Query.<T>}
98
+ */
99
+ Query.prototype.where = function(conditions) {
100
+ if (this._soql) {
101
+ throw Error("Cannot set where conditions for the query which has already built SOQL.");
102
+ }
103
+ this._config.conditions = conditions;
104
+ return this;
105
+ };
106
+
107
+ /**
108
+ * Limit the returning result
109
+ *
110
+ * @param {Number} limit - Maximum number of records the query will return.
111
+ * @returns {Query.<T>}
112
+ */
113
+ Query.prototype.limit = function(limit) {
114
+ if (this._soql) {
115
+ throw Error("Cannot set limit for the query which has already built SOQL.");
116
+ }
117
+ this._config.limit = limit;
118
+ return this;
119
+ };
120
+
121
+ /**
122
+ * Synonym of Query#offset()
123
+ *
124
+ * @method Query#skip
125
+ * @param {Number} offset - Offset number where begins returning results.
126
+ * @returns {Query.<T>}
127
+ */
128
+ /**
129
+ * Skip records
130
+ *
131
+ * @method Query#offset
132
+ * @param {Number} offset - Offset number where begins returning results.
133
+ * @returns {Query.<T>}
134
+ */
135
+ Query.prototype.skip =
136
+ Query.prototype.offset = function(offset) {
137
+ if (this._soql) {
138
+ throw Error("Cannot set skip/offset for the query which has already built SOQL.");
139
+ }
140
+ this._config.offset = offset;
141
+ return this;
142
+ };
143
+
144
+ /**
145
+ * Synonym of Query#sort()
146
+ *
147
+ * @memthod Query#orderby
148
+ * @param {String|Object} sort - Sorting field or hash object with field name and sord direction
149
+ * @param {String|Number} [dir] - Sorting direction (ASC|DESC|1|-1)
150
+ * @returns {Query.<T>}
151
+ */
152
+ /**
153
+ * Set query sort with direction
154
+ *
155
+ * @method Query#sort
156
+ * @param {String|Object} sort - Sorting field or hash object with field name and sord direction
157
+ * @param {String|Number} [dir] - Sorting direction (ASC|DESC|1|-1)
158
+ * @returns {Query.<T>}
159
+ */
160
+ Query.prototype.sort =
161
+ Query.prototype.orderby = function(sort, dir) {
162
+ if (this._soql) {
163
+ throw Error("Cannot set sort for the query which has already built SOQL.");
164
+ }
165
+ if (_.isString(sort) && _.isString(dir)) {
166
+ sort = [ [ sort, dir ] ];
167
+ }
168
+ this._config.sort = sort;
169
+ return this;
170
+ };
171
+
172
+ /**
173
+ * Include child relationship query
174
+ *
175
+ * @param {String} childRelName - Child relationship name to include in query result
176
+ * @param {Object|String} [conditions] - Conditions in JSON object (MongoDB-like), or raw SOQL WHERE clause string.
177
+ * @param {Object|Array.<String>|String} [fields] - Fields to fetch. Format can be in JSON object (MongoDB-like), array of field names, or comma-separated field names.
178
+ * @param {Object} [options] - Optional query configulations.
179
+ * @param {Number} [options.limit] - Maximum number of records the query will return.
180
+ * @param {Number} [options.offset] - Offset number where begins returning results.
181
+ * @param {Number} [options.skip] - Synonym of options.offset.
182
+ * @returns {Query~SubQuery}
183
+ */
184
+ Query.prototype.include = function(childRelName, conditions, fields, options) {
185
+ if (this._soql) {
186
+ throw Error("Cannot include child relationship into the query which has already built SOQL.");
187
+ }
188
+ if (_.isObject(childRelName)) {
189
+ var includes = childRelName;
190
+ for (var crname in includes) {
191
+ var config = includes[crname];
192
+ this.include(crname, config.conditions, config.fields, config);
193
+ }
194
+ return;
195
+ }
196
+ var childConfig = {
197
+ table: childRelName,
198
+ conditions: conditions,
199
+ fields: fields,
200
+ limit: options && options.limit,
201
+ offset: options && (options.offset || options.skip),
202
+ sort: options && options.sort
203
+ };
204
+ if (!_.isArray(this._config.includes)) this._config.includes = [];
205
+ this._config.includes.push(childConfig);
206
+ var childQuery = new SubQuery(this._conn, this, childConfig);
207
+ this._children = this._children || [];
208
+ this._children.push(childQuery);
209
+ return childQuery;
210
+ };
211
+
212
+
213
+ /**
214
+ * Setting maxFetch query option
215
+ *
216
+ * @param {Number} maxFetch - Max fetching records in auto fetch mode
217
+ * @returns {Query.<T>}
218
+ */
219
+ Query.prototype.maxFetch = function(maxFetch) {
220
+ this._options.maxFetch = maxFetch;
221
+ return this;
222
+ };
223
+
224
+ /**
225
+ * Switching auto fetch mode
226
+ *
227
+ * @param {Boolean} autoFetch - Using auto fetch mode or not
228
+ * @returns {Query.<T>}
229
+ */
230
+ Query.prototype.autoFetch = function(autoFetch) {
231
+ this._options.autoFetch = autoFetch;
232
+ return this;
233
+ };
234
+
235
+ /**
236
+ * Set flag to scan all records including deleted and archived.
237
+ *
238
+ * @param {Boolean} scanAll - Flag whether include deleted/archived record or not. Default is false.
239
+ * @returns {Query.<T>}
240
+ */
241
+ Query.prototype.scanAll = function(scanAll) {
242
+ this._options.scanAll = scanAll;
243
+ return this;
244
+ };
245
+
246
+ /**
247
+ * @private
248
+ */
249
+ var ResponseTargets = Query.ResponseTargets = {};
250
+ [ "QueryResult", "Records", "SingleRecord", "Count" ].forEach(function(f) {
251
+ ResponseTargets[f] = f;
252
+ });
253
+
254
+ /**
255
+ * @protected
256
+ * @param {String} responseTarget - Query response target
257
+ * @returns {Query.<S>}
258
+ */
259
+ Query.prototype.setResponseTarget = function(responseTarget) {
260
+ if (responseTarget in ResponseTargets) {
261
+ this._options.responseTarget = responseTarget;
262
+ }
263
+ return this;
264
+ };
265
+
266
+
267
+ /**
268
+ * Synonym of Query#execute()
269
+ *
270
+ * @method Query#run
271
+ * @param {Object} [options] - Query options
272
+ * @param {Boolean} [options.autoFetch] - Using auto fetch mode or not
273
+ * @param {Number} [options.maxFetch] - Max fetching records in auto fetch mode
274
+ * @param {Boolean} [options.scanAll] - Including deleted records for query target or not
275
+ * @param {Object} [options.headers] - Additional HTTP request headers sent in query request
276
+ * @param {Callback.<T>} [callback] - Callback function
277
+ * @returns {Query.<T>}
278
+ */
279
+ Query.prototype.run =
280
+ /**
281
+ * Synonym of Query#execute()
282
+ *
283
+ * @method Query#exec
284
+ * @param {Object} [options] - Query options
285
+ * @param {Boolean} [options.autoFetch] - Using auto fetch mode or not
286
+ * @param {Number} [options.maxFetch] - Max fetching records in auto fetch mode
287
+ * @param {Boolean} [options.scanAll] - Including deleted records for query target or not
288
+ * @param {Object} [options.headers] - Additional HTTP request headers sent in query request
289
+ * @param {Callback.<T>} [callback] - Callback function
290
+ * @returns {Query.<T>}
291
+ */
292
+ Query.prototype.exec =
293
+ /**
294
+ * Execute query and fetch records from server.
295
+ *
296
+ * @method Query#execute
297
+ * @param {Object} [options] - Query options
298
+ * @param {Boolean} [options.autoFetch] - Using auto fetch mode or not
299
+ * @param {Number} [options.maxFetch] - Max fetching records in auto fetch mode
300
+ * @param {Boolean} [options.scanAll] - Including deleted records for query target or not
301
+ * @param {Object} [options.headers] - Additional HTTP request headers sent in query request
302
+ * @param {Callback.<T>} [callback] - Callback function
303
+ * @returns {Query.<T>}
304
+ */
305
+ Query.prototype.execute = function(options, callback) {
306
+ var self = this;
307
+ var logger = this._conn._logger;
308
+ var deferred = this._deferred;
309
+
310
+ if (this._executed) {
311
+ deferred.reject(new Error("re-executing already executed query"));
312
+ return this;
313
+ }
314
+
315
+ if (this._finished) {
316
+ deferred.reject(new Error("executing already closed query"));
317
+ return this;
318
+ }
319
+
320
+ if (typeof options === "function") {
321
+ callback = options;
322
+ options = {};
323
+ }
324
+ options = options || {};
325
+ options = {
326
+ headers: options.headers || self._options.headers,
327
+ responseTarget: options.responseTarget || self._options.responseTarget,
328
+ autoFetch: options.autoFetch || self._options.autoFetch,
329
+ maxFetch: options.maxFetch || self._options.maxFetch,
330
+ scanAll: options.scanAll || self._options.scanAll
331
+ };
332
+
333
+ // callback and promise resolution;
334
+ var promiseCallback = function(err, res) {
335
+ if (_.isFunction(callback)) {
336
+ try {
337
+ res = callback(err, res);
338
+ err = null;
339
+ } catch(e) {
340
+ err = e;
341
+ }
342
+ }
343
+ if (err) {
344
+ deferred.reject(err);
345
+ } else {
346
+ deferred.resolve(res);
347
+ }
348
+ };
349
+ this.once('response', function(res) {
350
+ promiseCallback(null, res);
351
+ });
352
+ this.once('error', function(err) {
353
+ promiseCallback(err);
354
+ });
355
+
356
+ // collect fetched records in array
357
+ // only when response target is Records and
358
+ // either callback or chaining promises are available to this query.
359
+ this.once('fetch', function() {
360
+ if (options.responseTarget === ResponseTargets.Records && (self._chaining || callback)) {
361
+ logger.debug('--- collecting all fetched records ---');
362
+ var records = [];
363
+ var onRecord = function(record) {
364
+ records.push(record);
365
+ };
366
+ self.on('record', onRecord);
367
+ self.once('end', function() {
368
+ self.removeListener('record', onRecord);
369
+ self.emit('response', records, self);
370
+ });
371
+ }
372
+ });
373
+
374
+ // flag to prevent re-execution
375
+ this._executed = true;
376
+
377
+ // start actual query
378
+ logger.debug('>>> Query start >>>');
379
+ this._execute(options).then(function() {
380
+ logger.debug('*** Query finished ***');
381
+ }).fail(function(err) {
382
+ logger.debug('--- Query error ---');
383
+ self.emit('error', err);
384
+ });
385
+
386
+ // return Query instance for chaining
387
+ return this;
388
+ };
389
+
390
+ /**
391
+ * @private
392
+ */
393
+ Query.prototype._execute = function(options) {
394
+ var self = this;
395
+ var logger = this._conn._logger;
396
+ var responseTarget = options.responseTarget;
397
+ var autoFetch = options.autoFetch;
398
+ var maxFetch = options.maxFetch;
399
+ var scanAll = options.scanAll;
400
+
401
+ return Promise.resolve(
402
+ self._locator ?
403
+ self._conn._baseUrl() + "/query/" + self._locator :
404
+ self.toSOQL().then(function(soql) {
405
+ self.totalFetched = 0;
406
+ logger.debug("SOQL = " + soql);
407
+ return self._conn._baseUrl() + "/" + (scanAll ? "queryAll" : "query") + "?q=" + encodeURIComponent(soql);
408
+ })
409
+ ).then(function(url) {
410
+ return self._conn.request({
411
+ method: 'GET',
412
+ url: url,
413
+ headers: options.headers
414
+ });
415
+ }).then(function(data) {
416
+ self.emit("fetch");
417
+ self.totalSize = data.totalSize;
418
+ var res;
419
+ switch(responseTarget) {
420
+ case ResponseTargets.SingleRecord:
421
+ res = data.records && data.records.length > 0 ? data.records[0] : null;
422
+ break;
423
+ case ResponseTargets.Records:
424
+ res = data.records;
425
+ break;
426
+ case ResponseTargets.Count:
427
+ res = data.totalSize;
428
+ break;
429
+ default:
430
+ res = data;
431
+ }
432
+ // only fire response event when it should be notified per fetch
433
+ if (responseTarget !== ResponseTargets.Records) {
434
+ self.emit("response", res, self);
435
+ }
436
+
437
+ // streaming record instances
438
+ var numRecords = (data.records && data.records.length) || 0;
439
+ for (var i=0; i<numRecords; i++) {
440
+ if (self.totalFetched >= maxFetch) {
441
+ self._finished = true;
442
+ break;
443
+ }
444
+ var record = data.records[i];
445
+ self.push(record);
446
+ self.emit('record', record, self.totalFetched++, self);
447
+ }
448
+ if (data.nextRecordsUrl) {
449
+ self._locator = data.nextRecordsUrl.split('/').pop();
450
+ }
451
+ self._finished = self._finished || data.done || !autoFetch;
452
+ if (self._finished) {
453
+ self.push(null);
454
+ } else {
455
+ self._execute(options);
456
+ }
457
+ return res;
458
+ });
459
+ };
460
+
461
+ /**
462
+ * Readable stream implementation
463
+ *
464
+ * @override
465
+ * @private
466
+ */
467
+ Query.prototype._read = function(size) {
468
+ if (!this._finished && !this._executed) {
469
+ this.execute({ autoFetch: true });
470
+ }
471
+ };
472
+
473
+ /** @override **/
474
+ Query.prototype.on = function(e, fn) {
475
+ if (e === 'record') {
476
+ var self = this;
477
+ this.on('readable', function() {
478
+ while(self.read() !== null) {} // discard buffered records
479
+ });
480
+ }
481
+ return Query.super_.prototype.on.call(this, e, fn);
482
+ };
483
+
484
+ /** @override **/
485
+ Query.prototype.addListener = Query.prototype.on;
486
+
487
+
488
+ /**
489
+ * @private
490
+ */
491
+ Query.prototype._expandFields = function() {
492
+ if (this._soql) {
493
+ return Promise.reject(new Error("Cannot expand fields for the query which has already built SOQL."));
494
+ }
495
+ var self = this;
496
+ var logger = self._conn._logger;
497
+ var conn = this._conn;
498
+ var table = this._config.table;
499
+ var fields = this._config.fields || [];
500
+
501
+ logger.debug('_expandFields: table = ' + table + ', fields = ' + fields.join(', '));
502
+
503
+ return Promise.all([
504
+ Promise.resolve(self._parent ? findRelationTable(table) : table)
505
+ .then(function(table) {
506
+ return Promise.all(
507
+ _.map(fields, function(field) { return expandAsteriskField(table, field); })
508
+ ).then(function(expandedFields) {
509
+ self._config.fields = _.flatten(expandedFields);
510
+ });
511
+ }),
512
+ Promise.all(
513
+ _.map(self._children || [], function(childQuery) {
514
+ return childQuery._expandFields();
515
+ })
516
+ )
517
+ ]);
518
+
519
+ function findRelationTable(rname) {
520
+ var ptable = self._parent._config.table;
521
+ logger.debug('finding table for relation "' + rname + '" in "' + ptable + '"...');
522
+ return describeCache(ptable).then(function(sobject) {
523
+ var upperRname = rname.toUpperCase();
524
+ var childRelation = _.find(sobject.childRelationships, function(cr) {
525
+ return (cr.relationshipName || '').toUpperCase() === upperRname;
526
+ });
527
+ return childRelation ? childRelation.childSObject :
528
+ Promise.reject(new Error("No child relationship found: " + rname ));
529
+ });
530
+ }
531
+
532
+ function describeCache(table) {
533
+ logger.debug('describe cache: '+table);
534
+ var deferred = Promise.defer();
535
+ conn.describe$(table, function(err, sobject) {
536
+ logger.debug('... done.');
537
+ if (err) { deferred.reject(err); }
538
+ else { deferred.resolve(sobject); }
539
+ });
540
+ return deferred.promise;
541
+ }
542
+
543
+ function expandAsteriskField(table, field) {
544
+ logger.debug('expanding field "'+ field + '" in "' + table + '"...');
545
+ var fpath = field.split('.');
546
+ return fpath[fpath.length - 1] === '*' ?
547
+ describeCache(table).then(function(sobject) {
548
+ logger.debug('table '+table+'has been described');
549
+ if (fpath.length > 1) {
550
+ var rname = fpath.shift();
551
+ var rfield = _.find(sobject.fields, function(f) {
552
+ return f.relationshipName &&
553
+ f.relationshipName.toUpperCase() === rname.toUpperCase();
554
+ });
555
+ if (rfield) {
556
+ var rtable = rfield.referenceTo.length === 1 ? rfield.referenceTo[0] : 'Name';
557
+ return expandAsteriskField(rtable, fpath.join('.')).then(function(fpaths) {
558
+ return _.map(fpaths, function(fpath) { return rname + '.' + fpath; });
559
+ });
560
+ } else {
561
+ return [];
562
+ }
563
+ } else {
564
+ return _.map(sobject.fields, function(f) { return f.name; });
565
+ }
566
+ }) :
567
+ Promise.resolve([ field ]);
568
+ }
569
+ };
570
+
571
+ /**
572
+ * Explain plan for executing query
573
+ *
574
+ * @param {Callback.<ExplainInfo>} [callback] - Callback function
575
+ * @returns {Promise.<ExplainInfo>}
576
+ */
577
+ Query.prototype.explain = function(callback) {
578
+ var self = this;
579
+ var logger = this._conn._logger;
580
+ return self.toSOQL().then(function(soql) {
581
+ logger.debug("SOQL = " + soql);
582
+ var url = "/query/?explain=" + encodeURIComponent(soql);
583
+ return self._conn.request(url);
584
+ }).thenCall(callback);
585
+ };
586
+
587
+ /**
588
+ * Return SOQL expression for the query
589
+ *
590
+ * @param {Callback.<String>} [callback] - Callback function
591
+ * @returns {Promise.<String>}
592
+ */
593
+ Query.prototype.toSOQL = function(callback) {
594
+ var self = this;
595
+ return Promise.resolve(self._soql ||
596
+ self._expandFields().then(function() { return SOQLBuilder.createSOQL(self._config); })
597
+ ).thenCall(callback);
598
+ };
599
+
600
+ /**
601
+ * Create data stream of queried records.
602
+ * Automatically resume query if paused.
603
+ *
604
+ * @param {String} [type] - Type of outgoing data format. Currently 'csv' is default value and the only supported.
605
+ * @param {Object} [options] - Options passed to converter
606
+ * @returns {stream.Readable}
607
+ */
608
+ Query.prototype.stream = RecordStream.Serializable.prototype.stream;
609
+
610
+ /**
611
+ * Get record stream of queried records applying the given mapping function
612
+ *
613
+ * @param {RecordMapFunction} fn - Record mapping function
614
+ * @returns {RecordStream.Serializable}
615
+ */
616
+ Query.prototype.map = RecordStream.prototype.map;
617
+
618
+ /**
619
+ * Get record stream of queried records, applying the given filter function
620
+ *
621
+ * @param {RecordFilterFunction} fn - Record filtering function
622
+ * @returns {RecordStream.Serializable}
623
+ */
624
+ Query.prototype.filter = RecordStream.prototype.map;
625
+
626
+ /*
627
+ * Default threshold num of bulk API switching
628
+ */
629
+ var DEFAULT_BULK_THRESHOLD = 200;
630
+
631
+ /**
632
+ * Synonym of Query#destroy()
633
+ *
634
+ * @method Query#delete
635
+ * @param {String} [type] - SObject type. Required for SOQL based query object.
636
+ * @param {Callback.<Array.<RecordResult>>} [callback] - Callback function
637
+ * @returns {Promise.<Array.<RecordResult>>}
638
+ */
639
+ /**
640
+ * Synonym of Query#destroy()
641
+ *
642
+ * @method Query#del
643
+ * @param {String} [type] - SObject type. Required for SOQL based query object.
644
+ * @param {Callback.<Array.<RecordResult>>} [callback] - Callback function
645
+ * @returns {Promise.<Array.<RecordResult>>}
646
+ */
647
+ /**
648
+ * Delete queried records
649
+ *
650
+ * @method Query#destroy
651
+ * @param {String} [type] - SObject type. Required for SOQL based query object.
652
+ * @param {Object} [options] - Mass delete operation options
653
+ * @param {Boolean} [options.allowBulk] - Allow switching to Bulk API when the num of queried records reached to certain threshold. Default is true.
654
+ * @param {Number} [options.bulkThreshold] - Threshold num to switch to use Bulk API instead of usual `SObject#delete()` call. Default value is 200 after API ver 42.0, and 0.5 * `maxRequest` before API ver 42.0.
655
+ * @param {Callback.<Array.<RecordResult>>} [callback] - Callback function
656
+ * @returns {Promise.<Array.<RecordResult>>}
657
+ */
658
+ Query.prototype["delete"] =
659
+ Query.prototype.del =
660
+ Query.prototype.destroy = function(type, options, callback) {
661
+ if (typeof type === 'function') {
662
+ callback = type;
663
+ options = {};
664
+ type = null;
665
+ } else if (typeof type === 'object' && type !== null) {
666
+ callback = options;
667
+ options = type;
668
+ type = null;
669
+ }
670
+ options = options || {};
671
+ type = type || (this._config && this._config.table);
672
+ if (!type) {
673
+ throw new Error("SOQL based query needs SObject type information to bulk delete.");
674
+ }
675
+ // Set the threshold number to pass to bulk API
676
+ var thresholdNum =
677
+ options.allowBulk === false ?
678
+ -1 :
679
+ typeof options.bulkThreshold === 'number' ?
680
+ options.bulkThreshold :
681
+ // determine threshold if the connection version supports SObject collection API or not
682
+ (this._conn._ensureVersion(42) ? DEFAULT_BULK_THRESHOLD : this._conn.maxRequest / 2);
683
+ var self = this;
684
+ return new Promise(function(resolve, reject) {
685
+ var records = [];
686
+ var batch = null;
687
+ var handleRecord = function(rec) {
688
+ if (!rec.Id) {
689
+ self.emit('error', new Error('Queried record does not include Salesforce record ID.'))
690
+ return;
691
+ }
692
+ var record = { Id: rec.Id };
693
+ if (batch) {
694
+ batch.write(record);
695
+ } else {
696
+ records.push(record);
697
+ if (thresholdNum < 0 || records.length > thresholdNum) {
698
+ // Use bulk delete instead of SObject REST API
699
+ batch =
700
+ self._conn.sobject(type).deleteBulk()
701
+ .on('response', resolve)
702
+ .on('error', reject);
703
+ records.forEach(function(record) {
704
+ batch.write(record);
705
+ });
706
+ records = [];
707
+ }
708
+ }
709
+ };
710
+ var handleEnd = function() {
711
+ if (batch) {
712
+ batch.end();
713
+ } else {
714
+ var ids = records.map(function (record) { return record.Id; });
715
+ self._conn.sobject(type).destroy(ids, { allowRecursive: true }).then(resolve, reject);
716
+ }
717
+ };
718
+ self.on('data', handleRecord)
719
+ .on('end', handleEnd)
720
+ .on('error', reject);
721
+ }).thenCall(callback);
722
+ };
723
+
724
+ /**
725
+ * Update queried records, using given mapping function/object
726
+ *
727
+ * @param {Record|RecordMapFunction} mapping - Mapping record or record mapping function
728
+ * @param {String} [type] - SObject type. Required for SOQL based query object.
729
+ * @param {Object} [options] - Mass update operation options
730
+ * @param {Boolean} [options.allowBulk] - Allow switching to Bulk API when the num of queried records reached to certain threshold. Default is true.
731
+ * @param {Number} [options.bulkThreshold] - Threshold num to switch to use Bulk API instead of usual `SObject#delete()` call. Default value is 200 after API ver 42.0, and 0.5 * `maxRequest` before API ver 42.0.
732
+ * @param {Callback.<Array.<RecordResult>>} [callback] - Callback function
733
+ * @returns {Promise.<Array.<RecordResult>>}
734
+ */
735
+ Query.prototype.update = function(mapping, type, options, callback) {
736
+ if (typeof type === 'function') {
737
+ callback = type;
738
+ options = {};
739
+ type = null;
740
+ } else if (typeof type === 'object' && type !== null) {
741
+ callback = options;
742
+ options = type;
743
+ type = null;
744
+ }
745
+ options = options || {};
746
+ type = type || (this._config && this._config.table);
747
+ if (!type) {
748
+ throw new Error("SOQL based query needs SObject type information to bulk update.");
749
+ }
750
+ var updateStream = _.isFunction(mapping) ? RecordStream.map(mapping) : RecordStream.recordMapStream(mapping);
751
+ // Set the threshold number to pass to bulk API
752
+ var thresholdNum =
753
+ options.allowBulk === false ?
754
+ -1 :
755
+ typeof options.bulkThreshold === 'number' ?
756
+ options.bulkThreshold :
757
+ // determine threshold if the connection version supports SObject collection API or not
758
+ (this._conn._ensureVersion(42) ? DEFAULT_BULK_THRESHOLD : this._conn.maxRequest / 2);
759
+ var self = this;
760
+ return new Promise(function(resolve, reject) {
761
+ var records = [];
762
+ var batch = null;
763
+ var handleRecord = function(record) {
764
+ if (batch) {
765
+ batch.write(record);
766
+ } else {
767
+ records.push(record);
768
+ if (thresholdNum < 0 || records.length > thresholdNum) {
769
+ // Use bulk update instead of SObject REST API
770
+ batch =
771
+ self._conn.sobject(type).updateBulk()
772
+ .on('response', resolve)
773
+ .on('error', reject);
774
+ records.forEach(function(record) {
775
+ batch.write(record);
776
+ });
777
+ records = [];
778
+ }
779
+ }
780
+ };
781
+ var handleEnd = function() {
782
+ if (batch) {
783
+ batch.end();
784
+ } else {
785
+ self._conn.sobject(type).update(records, { allowRecursive: true }).then(resolve, reject);
786
+ }
787
+ };
788
+ self.on('error', reject)
789
+ .pipe(updateStream)
790
+ .on('data', handleRecord)
791
+ .on('end', handleEnd)
792
+ .on('error', reject);
793
+ }).thenCall(callback);
794
+ };
795
+
796
+ /**
797
+ * Promise/A+ interface
798
+ * http://promises-aplus.github.io/promises-spec/
799
+ *
800
+ * Delegate to deferred promise, return promise instance for query result
801
+ *
802
+ * @param {FulfilledCallback.<T, S1>} [onFulfilled]
803
+ * @param {RejectedCallback.<S2>} [onRejected]
804
+ * @returns {Promise.<S1|S2>}
805
+ */
806
+ Query.prototype.then = function(onResolved, onReject) {
807
+ this._chaining = true;
808
+ if (!this._finished && !this._executed) { this.execute(); }
809
+ return this._deferred.promise.then.apply(this._deferred.promise, arguments);
810
+ };
811
+
812
+ /**
813
+ * Promise/A+ extension
814
+ * Call "then" using given node-style callback function
815
+ *
816
+ * @param {Callback.<T>} [callback] - Callback function
817
+ * @returns {Query}
818
+ */
819
+ Query.prototype.thenCall = function(callback) {
820
+ if (_.isFunction(callback)) {
821
+ this.then(function(res) {
822
+ process.nextTick(function() {
823
+ callback(null, res);
824
+ });
825
+ }, function(err) {
826
+ process.nextTick(function() {
827
+ callback(err);
828
+ });
829
+ });
830
+ }
831
+ return this;
832
+ };
833
+
834
+ /*--------------------------------------------*/
835
+
836
+ /**
837
+ * SubQuery object for representing child relationship query
838
+ *
839
+ * @protected
840
+ * @class Query~SubQuery
841
+ * @extends Query
842
+ * @param {Connection} conn - Connection object
843
+ * @param {Query} parent - Parent query object
844
+ * @param {Object} config - Sub query configuration
845
+ */
846
+ var SubQuery = function(conn, parent, config) {
847
+ SubQuery.super_.call(this, conn, config);
848
+ this._parent = parent;
849
+ };
850
+
851
+ inherits(SubQuery, Query);
852
+
853
+ /**
854
+ * @method Query~SubQuery#include
855
+ * @override
856
+ */
857
+ SubQuery.prototype.include = function() {
858
+ throw new Error("Not allowed to include another subquery in subquery.");
859
+ };
860
+
861
+ /**
862
+ * Back the context to parent query object
863
+ *
864
+ * @method Query~SubQuery#end
865
+ * @returns {Query}
866
+ */
867
+ SubQuery.prototype.end = function() {
868
+ return this._parent;
869
+ };
870
+
871
+ /**
872
+ * If execute is called in subquery context, delegate it to parent query object
873
+ *
874
+ * @method Query~SubQuery#execute
875
+ * @override
876
+ */
877
+ SubQuery.prototype.run =
878
+ SubQuery.prototype.exec =
879
+ SubQuery.prototype.execute = function() {
880
+ return this._parent.execute.apply(this._parent, arguments);
881
+ };