├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── sfcoffee └── sfjs ├── examples ├── migration.js └── pipeline.js ├── index.js ├── lib ├── analytics.js ├── apex.js ├── bulk.js ├── cache.js ├── chatter.js ├── connection.js ├── csv.js ├── date.js ├── logger.js ├── metadata.js ├── oauth2.js ├── promise.js ├── query.js ├── record-stream.js ├── record.js ├── repl.js ├── request.js ├── salesforce.js ├── soap.js ├── sobject.js ├── soql-builder.js ├── streaming.js └── tooling.js ├── package.json └── test ├── analytics.test.js ├── apex.test.js ├── bulk.test.js ├── cache.test.js ├── chatter.test.js ├── config ├── .gitignore ├── oauth2.js.example └── salesforce.js.example ├── connection.test.js ├── data ├── .gitignore ├── Account.csv └── MyPackage.zip ├── helper ├── assert.js └── webauth.js ├── metadata.test.js ├── oauth2.test.js ├── query.test.js ├── sobject.test.js ├── soql-builder.test.js ├── streaming.test.js └── tooling.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | 9 | pids 10 | logs 11 | results 12 | 13 | node_modules 14 | npm-debug.log 15 | 16 | .npmignore 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | - 0.8 6 | branches: 7 | only: 8 | - travis-ci 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Shinichi Tomita ; 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-salesforce 2 | 3 | Node-Salesforce is moved and renamed to JSforce (https://github.com/jsforce/jsforce). -------------------------------------------------------------------------------- /bin/sfcoffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var repl = require('coffee-script/lib/coffee-script/repl'); 3 | var sfrepl = require('../lib/repl.js'); 4 | sfrepl(repl).start({}); -------------------------------------------------------------------------------- /bin/sfjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var repl = require('repl'); 3 | var sfrepl = require('../lib/repl.js'); 4 | sfrepl(repl).start({}); 5 | -------------------------------------------------------------------------------- /examples/migration.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var sf = require('../lib/salesforce'); 3 | 4 | var config = {};// { logLevel: "DEBUG" }; 5 | var conn1 = new sf.Connection(config); 6 | var conn2 = new sf.Connection(config); 7 | 8 | async.waterfall([ 9 | function(next) { 10 | async.parallel([ 11 | function(cb) { 12 | conn1.login(process.env.SF_USERNAME_1, process.env.SF_PASSWORD_1, cb); 13 | }, 14 | function(cb) { 15 | conn2.login(process.env.SF_USERNAME_2, process.env.SF_PASSWORD_2, cb); 16 | } 17 | ], next); 18 | }, 19 | function(rets, next) { 20 | conn2.sobject('Account').count(next); 21 | }, 22 | function(cnt, next) { 23 | console.log("Account count in conn2 : " + cnt); 24 | async.parallel([ 25 | function(cb) { 26 | conn1.sobject('Account').describe(cb); 27 | }, 28 | function(cb) { 29 | conn2.sobject('Account').describe(cb); 30 | } 31 | ], next); 32 | }, 33 | function(sobjects, next) { 34 | var so1 = sobjects[0], so2 = sobjects[1]; 35 | var fields1 = {}; 36 | so1.fields.forEach(function(f) { fields1[f.name] = 1; }); 37 | var fields2 = {}; 38 | so2.fields.forEach(function(f) { 39 | if (fields1[f.name] && f.updateable && !f.custom && f.type !== 'reference') { 40 | fields2[f.name] = 1; 41 | } 42 | }); 43 | 44 | conn1.sobject('Account').find({}, fields2) 45 | .pipe(conn2.bulk.load('Account', 'insert')) 46 | .on('response', function(res) { next(null, res); }) 47 | .on('error', function(err){ next(err); }); 48 | }, 49 | function(rets, next) { 50 | var success = rets.filter(function(r) { return r.success; }).length; 51 | var failure = rets.length - success; 52 | console.log("bulkload sucess = " + success + ", failure = " + failure); 53 | conn2.sobject('Account').count(next); 54 | }, 55 | function(cnt, next) { 56 | console.log("Account count in conn2 : " + cnt); 57 | conn2.sobject('Account').find({ CreatedDate : sf.Date.TODAY }).exec(next); 58 | }, 59 | function(records, next) { 60 | console.log("deleting created records ("+records.length+")"); 61 | conn2.bulk.load('Account', 'delete', records, next); 62 | }, 63 | function(rets, next) { 64 | var success = rets.filter(function(r) { return r.success; }).length; 65 | var failure = rets.length - success; 66 | console.log("delete sucess = " + success + ", failure = " + failure); 67 | conn2.sobject('Account').count(next); 68 | }, 69 | function(cnt, next) { 70 | console.log("Account count in conn2 : " + cnt); 71 | next(); 72 | } 73 | ], function(err, res) { 74 | if (err) { 75 | console.error(err); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /examples/pipeline.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var async = require('async'); 3 | var sf = require('../lib/salesforce'); 4 | 5 | var config = {};// { logLevel: "DEBUG" }; 6 | var conn = new sf.Connection(config); 7 | 8 | var Opportunity = conn.sobject('Opportunity'); 9 | 10 | async.waterfall([ 11 | function(next) { 12 | conn.login(process.env.SF_USERNAME, process.env.SF_PASSWORD, next); 13 | }, 14 | function(sobjects, next) { 15 | Opportunity.find({ AccountId: { $ne: null }}, { Id: 1, Name: 1, "Account.Name": 1 }) 16 | .pipe(sf.RecordStream.map(function(r) { 17 | r.Name = r.Account.Name + ' *** ' + r.Name; 18 | return r; 19 | })) 20 | .pipe(Opportunity.updateBulk()) 21 | .on('response', function(rets) { 22 | next(null, rets); 23 | }) 24 | .on('error', function(err) { 25 | next(err); 26 | }); 27 | 28 | }, 29 | function(rets, next) { 30 | var success = rets.filter(function(r) { return r.success; }).length; 31 | var failure = rets.length - success; 32 | console.log("bulk update sucess = " + success + ", failure = " + failure); 33 | next(); 34 | }, 35 | function(next) { 36 | Opportunity.find({ Name : { $like: '% *** %' }}, { Id: 1, Name: 1 }) 37 | .pipe(sf.RecordStream.map(function(r) { 38 | r.Name = r.Name.replace(/^.+ \*\*\* /g, ''); 39 | return r; 40 | })) 41 | .pipe(Opportunity.updateBulk()) 42 | .on('response', function(rets) { 43 | next(null, rets); 44 | }) 45 | .on('error', function(err) { 46 | next(err); 47 | }); 48 | }, 49 | function(rets, next) { 50 | var success = rets.filter(function(r) { return r.success; }).length; 51 | var failure = rets.length - success; 52 | console.log("bulk update sucess = " + success + ", failure = " + failure); 53 | next(); 54 | }, 55 | function(next) { 56 | Opportunity 57 | .find({}, { Id: 1, Name: 1, Amount: 1, StageName: 1, CreatedDate: 1 }) 58 | .pipe(sf.RecordStream.filter(function(r) { 59 | return r.Amount > 500000; 60 | })) 61 | .stream().pipe(fs.createWriteStream("opps.csv")) 62 | .on('end', function() { next(); }) 63 | .on('error', function(err) { next(err); }); 64 | } 65 | ], function(err, res) { 66 | if (err) { 67 | console.error(err); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/salesforce'); 2 | -------------------------------------------------------------------------------- /lib/analytics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages Salesforce Analytics API 3 | * @author Shinichi Tomita 4 | */ 5 | 6 | var _ = require('underscore')._, 7 | Promise = require('./promise'); 8 | 9 | /** 10 | * Report instance to retrieving asynchronously executed result 11 | * 12 | * @protected 13 | * @class Analytics~ReportInstance 14 | * @param {Analytics~Report} report - Report 15 | * @param {String} id - Report instance id 16 | */ 17 | var ReportInstance = function(report, id) { 18 | this._report = report; 19 | this._conn = report._conn; 20 | this.id = id; 21 | }; 22 | 23 | /** 24 | * Retrieve report result asynchronously executed 25 | * 26 | * @method Analytics~ReportInstance#retrieve 27 | * @param {Callback.} [callback] - Callback function 28 | * @returns {Promise.} 29 | */ 30 | ReportInstance.prototype.retrieve = function(callback) { 31 | var conn = this._conn, 32 | report = this._report; 33 | var url = [ conn._baseUrl(), "analytics", "reports", report.id, "instances", this.id ].join('/'); 34 | return conn._request(url).thenCall(callback); 35 | }; 36 | 37 | /** 38 | * Report object in Analytics API 39 | * 40 | * @protected 41 | * @class Analytics~Report 42 | * @param {Connection} conn Connection 43 | */ 44 | var Report = function(conn, id) { 45 | this._conn = conn; 46 | this.id = id; 47 | }; 48 | 49 | /** 50 | * Describe report metadata 51 | * 52 | * @method Analytics~Report#describe 53 | * @param {Callback.} [callback] - Callback function 54 | * @returns {Promise.} 55 | */ 56 | Report.prototype.describe = function(callback) { 57 | var url = [ this._conn._baseUrl(), "analytics", "reports", this.id, "describe" ].join('/'); 58 | return this._conn._request(url).thenCall(callback); 59 | }; 60 | 61 | /** 62 | * Run report synchronously 63 | * 64 | * @method Analytics~Report#execute 65 | * @param {Object} [options] - Options 66 | * @param {Boolean} options.details - Flag if include detail in result 67 | * @param {Analytics~ReportMetadata} options.metadata - Overriding report metadata 68 | * @param {Callback.} [callback] - Callback function 69 | * @returns {Promise.} 70 | */ 71 | Report.prototype.run = 72 | Report.prototype.exec = 73 | Report.prototype.execute = function(options, callback) { 74 | options = options || {}; 75 | if (_.isFunction(options)) { 76 | callback = options; 77 | options = {}; 78 | } 79 | var url = [ this._conn._baseUrl(), "analytics", "reports", this.id ].join('/'); 80 | if (options.details) { 81 | url += "?includeDetails=true"; 82 | } 83 | var params = { method : options.metadata ? 'POST' : 'GET', url : url }; 84 | if (options.metadata) { 85 | params.headers = { "Content-Type" : "application/json" }; 86 | params.body = JSON.stringify(options.metadata); 87 | } 88 | return this._conn._request(params).thenCall(callback); 89 | }; 90 | 91 | /** 92 | * Run report asynchronously 93 | * 94 | * @method Analytics~Report#executeAsync 95 | * @param {Object} [options] - Options 96 | * @param {Boolean} options.details - Flag if include detail in result 97 | * @param {Analytics~ReportMetadata} options.metadata - Overriding report metadata 98 | * @param {Callback.} [callback] - Callback function 99 | * @returns {Promise.} 100 | */ 101 | Report.prototype.executeAsync = function(options, callback) { 102 | options = options || {}; 103 | if (_.isFunction(options)) { 104 | callback = options; 105 | options = {}; 106 | } 107 | var url = [ this._conn._baseUrl(), "analytics", "reports", this.id, "instances" ].join('/'); 108 | if (options.details) { 109 | url += "?includeDetails=true"; 110 | } 111 | var params = { method : 'POST', url : url }; 112 | if (options.metadata) { 113 | params.headers = { "Content-Type" : "application/json" }; 114 | params.body = JSON.stringify(options.metadata); 115 | } 116 | return this._conn._request(params).thenCall(callback); 117 | }; 118 | 119 | /** 120 | * Get report instance for specified instance ID 121 | * 122 | * @method Analytics~Report#instance 123 | * @param {String} id - Report instance ID 124 | * @returns {Analytics~ReportInstance} 125 | */ 126 | Report.prototype.instance = function(id) { 127 | return new ReportInstance(this, id); 128 | }; 129 | 130 | /** 131 | * List report instances which had been executed asynchronously 132 | * 133 | * @method Analytics~Report#instances 134 | * @param {Callback.>} [callback] - Callback function 135 | * @returns {Promise.>} 136 | */ 137 | Report.prototype.instances = function(callback) { 138 | var url = [ this._conn._baseUrl(), "analytics", "reports", this.id, "instances" ].join('/'); 139 | return this._conn._request(url).thenCall(callback); 140 | }; 141 | 142 | 143 | /** 144 | * API class for Analytics API 145 | * 146 | * @class 147 | * @param {Connection} conn Connection 148 | */ 149 | var Analytics = function(conn) { 150 | this._conn = conn; 151 | }; 152 | 153 | /** 154 | * Get report object of Analytics API 155 | * 156 | * @param {String} id - Report Id 157 | * @returns {Analytics~Report} 158 | */ 159 | Analytics.prototype.report = function(id) { 160 | return new Report(this._conn, id); 161 | }; 162 | 163 | /** 164 | * Get recent report list 165 | * 166 | * @param {Callback.>} [callback] - Callback function 167 | * @returns {Promise.>} 168 | */ 169 | Analytics.prototype.reports = function(callback) { 170 | var url = [ this._conn._baseUrl(), "analytics", "reports" ].join('/'); 171 | return this._conn._request(url).thenCall(callback); 172 | }; 173 | 174 | module.exports = Analytics; 175 | -------------------------------------------------------------------------------- /lib/apex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages Salesforce Apex REST endpoint calls 3 | * @author Shinichi Tomita 4 | */ 5 | 6 | /** 7 | * API class for Apex REST endpoint call 8 | * 9 | * @class 10 | * @param {Connection} conn Connection 11 | */ 12 | var Apex = function(conn) { 13 | this._conn = conn; 14 | }; 15 | 16 | /** 17 | * @private 18 | */ 19 | Apex.prototype._baseUrl = function() { 20 | return this._conn.instanceUrl + "/services/apexrest"; 21 | }; 22 | 23 | /** 24 | * @private 25 | */ 26 | Apex.prototype._createRequestParams = function(method, path, body) { 27 | var params = { 28 | method: method, 29 | url: this._baseUrl() + path 30 | }; 31 | if (!/^(GET|DELETE)$/i.test(method)) { 32 | params.headers = { 33 | "Content-Type" : "application/json" 34 | }; 35 | } 36 | if (body) { 37 | params.body = JSON.stringify(body); 38 | } 39 | return params; 40 | }; 41 | 42 | /** 43 | * Call Apex REST service in GET request 44 | * 45 | * @param {String} path - URL path to Apex REST service 46 | * @param {Callback.} [callback] - Callback function 47 | * @returns {Promise.} 48 | */ 49 | Apex.prototype.get = function(path, callback) { 50 | return this._conn._request(this._createRequestParams('GET', path)).thenCall(callback); 51 | }; 52 | 53 | /** 54 | * Call Apex REST service in POST request 55 | * 56 | * @param {String} path - URL path to Apex REST service 57 | * @param {Object} [body] - Request body 58 | * @param {Callback.} [callback] - Callback function 59 | * @returns {Promise.} 60 | */ 61 | Apex.prototype.post = function(path, body, callback) { 62 | if (typeof body === 'function') { 63 | callback = body; 64 | body = undefined; 65 | } 66 | var params = this._createRequestParams('POST', path, body); 67 | return this._conn._request(params).thenCall(callback); 68 | }; 69 | 70 | /** 71 | * Call Apex REST service in PUT request 72 | * 73 | * @param {String} path - URL path to Apex REST service 74 | * @param {Object} [body] - Request body 75 | * @param {Callback.} [callback] - Callback function 76 | * @returns {Promise.} 77 | */ 78 | Apex.prototype.put = function(path, body, callback) { 79 | if (typeof body === 'function') { 80 | callback = body; 81 | body = undefined; 82 | } 83 | var params = this._createRequestParams('PUT', path, body); 84 | return this._conn._request(params).thenCall(callback); 85 | }; 86 | 87 | /** 88 | * Call Apex REST service in PATCH request 89 | * 90 | * @param {String} path - URL path to Apex REST service 91 | * @param {Object} [body] - Request body 92 | * @param {Callback.} [callback] - Callback function 93 | * @returns {Promise.} 94 | */ 95 | Apex.prototype.patch = function(path, body, callback) { 96 | if (typeof body === 'function') { 97 | callback = body; 98 | body = undefined; 99 | } 100 | var params = this._createRequestParams('PATCH', path, body); 101 | return this._conn._request(params).thenCall(callback); 102 | }; 103 | 104 | /** 105 | * Synonym of Apex#delete() 106 | * 107 | * @method Apex#del 108 | * 109 | * @param {String} path - URL path to Apex REST service 110 | * @param {Object} [body] - Request body 111 | * @param {Callback.} [callback] - Callback function 112 | * @returns {Promise.} 113 | */ 114 | /** 115 | * Call Apex REST service in DELETE request 116 | * 117 | * @method Apex#delete 118 | * 119 | * @param {String} path - URL path to Apex REST service 120 | * @param {Object} [body] - Request body 121 | * @param {Callback.} [callback] - Callback function 122 | * @returns {Promise.} 123 | */ 124 | Apex.prototype.del = 125 | Apex.prototype["delete"] = function(path, callback) { 126 | return this._conn._request(this._createRequestParams('DELETE', path)).thenCall(callback); 127 | }; 128 | 129 | 130 | module.exports = Apex; 131 | -------------------------------------------------------------------------------- /lib/bulk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages Salesforce Bulk API related operations 3 | * @author Shinichi Tomita 4 | */ 5 | 6 | var util = require('util'), 7 | stream = require('stream'), 8 | Stream = stream.Stream, 9 | events = require('events'), 10 | _ = require('underscore')._, 11 | Connection = require('./connection'), 12 | RecordStream = require('./record-stream'), 13 | CSV = require('./csv'), 14 | Promise = require('./promise'); 15 | 16 | 17 | /*--------------------------------------------*/ 18 | 19 | /** 20 | * Class for Bulk API Job 21 | * 22 | * @protected 23 | * @class Bulk~Job 24 | * @extends events.EventEmitter 25 | * 26 | * @param {Bulk} bulk - Bulk API object 27 | * @param {String} [type] - SObject type 28 | * @param {String} [operation] - Bulk load operation ('insert', 'update', 'upsert', 'delete', or 'hardDelete') 29 | * @param {Object} [options] - Options for bulk loading operation 30 | * @param {String} [options.extIdField] - External ID field name (used when upsert operation). 31 | * @param {String} [jobId] - Job ID (if already available) 32 | */ 33 | var Job = function(bulk, type, operation, options, jobId) { 34 | this._bulk = bulk; 35 | this.type = type; 36 | this.operation = operation; 37 | this.options = options || {}; 38 | this.id = jobId; 39 | this.state = this.id ? 'Open' : 'Unknown'; 40 | this._batches = {}; 41 | }; 42 | 43 | util.inherits(Job, events.EventEmitter); 44 | 45 | /** 46 | * Open new job and get jobinfo 47 | * 48 | * @method Bulk~Job#open 49 | * @param {Callback.} [callback] - Callback function 50 | * @returns {Promise.} 51 | */ 52 | Job.prototype.open = function(callback) { 53 | var self = this; 54 | var bulk = this._bulk; 55 | var logger = bulk._logger; 56 | 57 | // if not requested opening job 58 | if (!this._jobInfo) { 59 | var body = [ 60 | '', 61 | '', 62 | '' + this.operation.toLowerCase() + '', 63 | '' + this.type + '', 64 | (this.options.extIdField ? 65 | ''+this.options.extIdField+'' : 66 | ''), 67 | 'CSV', 68 | '' 69 | ].join(''); 70 | 71 | this._jobInfo = bulk._request({ 72 | method : 'POST', 73 | path : "/job", 74 | body : body, 75 | headers : { 76 | "Content-Type" : "application/xml; charset=utf-8" 77 | }, 78 | responseType: "application/xml" 79 | }).then(function(res) { 80 | self.emit("open", res.jobInfo); 81 | self.id = res.jobInfo.id; 82 | self.state = res.jobInfo.state; 83 | return res.jobInfo; 84 | }, function(err) { 85 | self.emit("error", err); 86 | throw err; 87 | }); 88 | } 89 | return this._jobInfo.thenCall(callback); 90 | }; 91 | 92 | /** 93 | * Create a new batch instance in the job 94 | * 95 | * @method Bulk~Job#createBatch 96 | * @returns {Bulk~Batch} 97 | */ 98 | Job.prototype.createBatch = function() { 99 | var batch = new Batch(this); 100 | var self = this; 101 | batch.on('queue', function() { 102 | self._batches[batch.id] = batch; 103 | }); 104 | return batch; 105 | }; 106 | 107 | /** 108 | * Get a batch instance specified by given batch ID 109 | * 110 | * @method Bulk~Job#batch 111 | * @param {String} batchId - Batch ID 112 | * @returns {Bulk~Batch} 113 | */ 114 | Job.prototype.batch = function(batchId) { 115 | var batch = this._batches[batchId]; 116 | if (!batch) { 117 | batch = new Batch(this, batchId); 118 | this._batches[batchId] = batch; 119 | } 120 | return batch; 121 | }; 122 | 123 | /** 124 | * Check the job status from server 125 | * 126 | * @method Bulk~Job#check 127 | * @param {Callback.} [callback] - Callback function 128 | * @returns {Promise.} 129 | */ 130 | Job.prototype.check = function(callback) { 131 | var self = this; 132 | var bulk = this._bulk; 133 | var logger = bulk._logger; 134 | 135 | return this.open().then(function() { 136 | return bulk._request({ 137 | method : 'GET', 138 | path : "/job/" + self.id, 139 | responseType: "application/xml" 140 | }); 141 | }).then(function(res) { 142 | logger.debug(res.jobInfo); 143 | self.state = res.jobInfo.state; 144 | return res.jobInfo; 145 | }).thenCall(callback); 146 | 147 | }; 148 | 149 | /** 150 | * List all registered batch info in job 151 | * 152 | * @method Bulk~Job#list 153 | * @param {Callback.>} [callback] - Callback function 154 | * @returns {Promise.>} 155 | */ 156 | Job.prototype.list = function(callback) { 157 | var self = this; 158 | var bulk = this._bulk; 159 | var logger = bulk._logger; 160 | 161 | return this.open().then(function() { 162 | return bulk._request({ 163 | method : 'GET', 164 | path : "/job/" + self.id + "/batch", 165 | responseType: "application/xml" 166 | }); 167 | }).then(function(res) { 168 | logger.debug(res.batchInfoList.batchInfo); 169 | var batchInfoList = res.batchInfoList; 170 | batchInfoList = _.isArray(batchInfoList.batchInfo) ? batchInfoList.batchInfo : [ batchInfoList.batchInfo ]; 171 | return batchInfoList; 172 | }).thenCall(callback); 173 | 174 | }; 175 | 176 | /** 177 | * Close opened job 178 | * 179 | * @method Bulk~Job#close 180 | * @param {Callback.} [callback] - Callback function 181 | * @returns {Promise.} 182 | */ 183 | Job.prototype.close = function() { 184 | var self = this; 185 | return this._changeState("Closed").then(function(jobInfo) { 186 | self.id = null; 187 | self.emit("close", jobInfo); 188 | return jobInfo; 189 | }, function(err) { 190 | self.emit("error", err); 191 | throw err; 192 | }); 193 | }; 194 | 195 | /** 196 | * Set the status to abort 197 | * 198 | * @method Bulk~Job#abort 199 | * @param {Callback.} [callback] - Callback function 200 | * @returns {Promise.} 201 | */ 202 | Job.prototype.abort = function() { 203 | var self = this; 204 | return this._changeState("Aborted").then(function(jobInfo) { 205 | self.id = null; 206 | self.emit("abort", jobInfo); 207 | self.state = "Aborted"; 208 | return jobInfo; 209 | }, function(err) { 210 | self.emit("error", err); 211 | throw err; 212 | }); 213 | }; 214 | 215 | /** 216 | * @private 217 | */ 218 | Job.prototype._changeState = function(state, callback) { 219 | var self = this; 220 | var bulk = this._bulk; 221 | var logger = bulk._logger; 222 | 223 | return this.open().then(function() { 224 | var body = [ 225 | '', 226 | '', 227 | '' + state + '', 228 | '' 229 | ].join(''); 230 | return bulk._request({ 231 | method : 'POST', 232 | path : "/job/" + self.id, 233 | body : body, 234 | headers : { 235 | "Content-Type" : "application/xml; charset=utf-8" 236 | }, 237 | responseType: "application/xml" 238 | }); 239 | }).then(function(res) { 240 | logger.debug(res.jobInfo); 241 | self.state = res.jobInfo.state; 242 | return res.jobInfo; 243 | }).thenCall(callback); 244 | 245 | }; 246 | 247 | 248 | /*--------------------------------------------*/ 249 | 250 | /** 251 | * Batch (extends RecordStream implements Sendable) 252 | * 253 | * @protected 254 | * @class Bulk~Batch 255 | * @extends {RecordStream} 256 | * @implements {Promise.>} 257 | * @param {Bulk~Job} job - Bulk job object 258 | * @param {String} [batchId] - Batch ID (if already available) 259 | */ 260 | var Batch = function(job, batchId) { 261 | Batch.super_.apply(this); 262 | this.sendable = true; 263 | this.job = job; 264 | this.id = batchId; 265 | this._bulk = job._bulk; 266 | this._csvStream = new RecordStream.CSVStream(); 267 | this._csvStream.stream().pipe(this.stream()); 268 | this._deferred = Promise.defer(); 269 | }; 270 | 271 | util.inherits(Batch, RecordStream); 272 | 273 | /** 274 | * Execute batch operation 275 | * 276 | * @method Bulk~Batch#execute 277 | * @param {Array.|stream.Stream|String} [input] - Input source for batch operation. Accepts array of records, CSv string, and CSV data input stream. 278 | * @param {Callback.>} [callback] - Callback function 279 | * @returns {Bulk~Batch} 280 | */ 281 | Batch.prototype.run = 282 | Batch.prototype.exec = 283 | Batch.prototype.execute = function(input, callback) { 284 | var self = this; 285 | 286 | if (typeof input === 'function') { // if input argument is omitted 287 | callback = input; 288 | input = null; 289 | } 290 | 291 | // if batch is already executed 292 | if (this._result) { 293 | throw new Error("Batch already executed."); 294 | } 295 | 296 | var rdeferred = Promise.defer(); 297 | this._result = rdeferred.promise; 298 | this._result.thenCall(callback).then(function(res) { 299 | self._deferred.resolve(res); 300 | }, function(err) { 301 | self._deferred.reject(err); 302 | }); 303 | this.once('response', function(res) { 304 | rdeferred.resolve(res); 305 | }); 306 | this.once('error', function(err) { 307 | rdeferred.reject(err); 308 | }); 309 | 310 | if (input instanceof Stream) { 311 | input.pipe(this.stream()); 312 | } else { 313 | var data; 314 | if (_.isArray(input)) { 315 | _.forEach(input, function(record) { self.send(record); }); 316 | } else if (_.isString(input)){ 317 | data = input; 318 | var stream = this.stream(); 319 | stream.write(data); 320 | stream.end(); 321 | } 322 | } 323 | 324 | // return Batch instance for chaining 325 | return this; 326 | }; 327 | 328 | /** 329 | * Promise/A+ interface 330 | * http://promises-aplus.github.io/promises-spec/ 331 | * 332 | * Delegate to deferred promise, return promise instance for batch result 333 | * 334 | * @method Bulk~Batch#then 335 | */ 336 | Batch.prototype.then = function(onResolved, onReject, onProgress) { 337 | return this._deferred.promise.then(onResolved, onReject, onProgress); 338 | }; 339 | 340 | /** 341 | * Promise/A+ extension 342 | * Call "then" using given node-style callback function 343 | * 344 | * @method Bulk~Batch#thenCall 345 | */ 346 | Batch.prototype.thenCall = function(callback) { 347 | return _.isFunction(callback) ? this.then(function(res) { 348 | return callback(null, res); 349 | }, function(err) { 350 | return callback(err); 351 | }) : this; 352 | }; 353 | 354 | /** 355 | * @override 356 | */ 357 | Batch.prototype.send = function(record) { 358 | record = _.clone(record); 359 | if (this.job.operation === "insert") { 360 | delete record.Id; 361 | } else if (this.job.operation === "delete") { 362 | record = { Id: record.Id }; 363 | } 364 | delete record.type; 365 | delete record.attributes; 366 | return this._csvStream.send(record); 367 | }; 368 | 369 | /** 370 | * @override 371 | */ 372 | Batch.prototype.end = function(record) { 373 | if (record) { 374 | this.send(record); 375 | } 376 | this.sendable = false; 377 | this._csvStream.end(); 378 | }; 379 | 380 | 381 | 382 | /** 383 | * Check batch status in server 384 | * 385 | * @method Bulk~Batch#check 386 | * @param {Callback.} [callback] - Callback function 387 | * @returns {Promise.} 388 | */ 389 | Batch.prototype.check = function(callback) { 390 | var self = this; 391 | var bulk = this._bulk; 392 | var logger = bulk._logger; 393 | var jobId = this.job.id; 394 | var batchId = this.id; 395 | 396 | if (!jobId || !batchId) { 397 | throw new Error("Batch not started."); 398 | } 399 | return bulk._request({ 400 | method : 'GET', 401 | path : "/job/" + jobId + "/batch/" + batchId, 402 | responseType: "application/xml" 403 | }).then(function(res) { 404 | logger.debug(res.batchInfo); 405 | return res.batchInfo; 406 | }).thenCall(callback); 407 | }; 408 | 409 | 410 | /** 411 | * Polling the batch result and retrieve 412 | * 413 | * @method Bulk~Batch#poll 414 | * @param {Number} interval - Polling interval in milliseconds 415 | * @param {Number} timeout - Polling timeout in milliseconds 416 | */ 417 | Batch.prototype.poll = function(interval, timeout) { 418 | var self = this; 419 | var jobId = this.job.id; 420 | var batchId = this.id; 421 | 422 | if (!jobId || !batchId) { 423 | throw new Error("Batch not started."); 424 | } 425 | var startTime = new Date().getTime(); 426 | var poll = function() { 427 | var now = new Date().getTime(); 428 | if (startTime + timeout < now) { 429 | self.emit('error', new Error("polling time out")); 430 | return; 431 | } 432 | self.check(function(err, res) { 433 | if (err) { 434 | self.emit('error', err); 435 | } else { 436 | if (res.state === "Failed") { 437 | if (parseInt(res.numberRecordsProcessed, 10) > 0) { 438 | self.retrieve(); 439 | } else { 440 | self.emit('error', new Error(res.stateMessage)); 441 | } 442 | } else if (res.state === "Completed") { 443 | self.retrieve(); 444 | } else { 445 | setTimeout(poll, interval); 446 | } 447 | } 448 | }); 449 | }; 450 | setTimeout(poll, interval); 451 | }; 452 | 453 | /** 454 | * Retrieve batch result 455 | * 456 | * @method Bulk~Batch#retrieve 457 | * @param {Callback.>} [callback] - Callback function 458 | * @returns {Promise.>} 459 | */ 460 | Batch.prototype.retrieve = function(callback) { 461 | var self = this; 462 | var bulk = this._bulk; 463 | var jobId = this.job.id; 464 | var batchId = this.id; 465 | 466 | if (!jobId || !batchId) { 467 | throw new Error("Batch not started."); 468 | } 469 | return bulk._request({ 470 | method : 'GET', 471 | path : "/job/" + jobId + "/batch/" + batchId + "/result" 472 | }).then(function(results) { 473 | results = _.map(results, function(ret) { 474 | return { 475 | id: ret.Id || null, 476 | success: ret.Success === "true", 477 | errors: ret.Error ? [ ret.Error ] : [] 478 | }; 479 | }); 480 | self.emit('response', results); 481 | return results; 482 | }, function(err) { 483 | self.emit('error', err); 484 | throw err; 485 | }).thenCall(callback); 486 | }; 487 | 488 | /** 489 | * @override 490 | */ 491 | Batch.prototype.stream = function() { 492 | if (!this._stream) { 493 | this._stream = new BatchStream(this); 494 | } 495 | return this._stream; 496 | }; 497 | 498 | /*--------------------------------------------*/ 499 | 500 | /** 501 | * Batch uploading stream (extends WritableStream) 502 | * 503 | * @private 504 | * @class Bulk~BatchStream 505 | * @extends stream.Stream 506 | */ 507 | var BatchStream = function(batch) { 508 | BatchStream.super_.call(this); 509 | this.batch = batch; 510 | this.writable = true; 511 | }; 512 | 513 | util.inherits(BatchStream, Stream); 514 | 515 | /** 516 | * @private 517 | */ 518 | BatchStream.prototype._getRequestStream = function() { 519 | var batch = this.batch; 520 | var bulk = batch._bulk; 521 | var logger = bulk._logger; 522 | 523 | if (!this._reqStream) { 524 | this._reqStream = bulk._request({ 525 | method : 'POST', 526 | path : "/job/" + batch.job.id + "/batch", 527 | headers: { 528 | "Content-Type": "text/csv" 529 | }, 530 | responseType: "application/xml" 531 | }, function(err, res) { 532 | if (err) { 533 | batch.emit('error', err); 534 | } else { 535 | logger.debug(res.batchInfo); 536 | batch.id = res.batchInfo.id; 537 | batch.emit('queue', res.batchInfo); 538 | } 539 | }).stream(); 540 | } 541 | return this._reqStream; 542 | }; 543 | 544 | /** 545 | * @override 546 | */ 547 | BatchStream.prototype.write = function(data) { 548 | var batch = this.batch; 549 | if (!batch.job.id) { 550 | this._queue(data); 551 | return; 552 | } 553 | return this._getRequestStream().write(data); 554 | }; 555 | 556 | /** 557 | * @override 558 | */ 559 | BatchStream.prototype.end = function(data) { 560 | var batch = this.batch; 561 | if (!batch.job.id) { 562 | this._ending = true; 563 | if (data) { 564 | this._queue(data); 565 | } 566 | return; 567 | } 568 | this.writable = false; 569 | this._getRequestStream().end(data); 570 | }; 571 | 572 | /** 573 | * @private 574 | */ 575 | BatchStream.prototype._queue = function(data) { 576 | var bstream = this; 577 | var batch = this.batch; 578 | var job = batch.job; 579 | if (!this._buffer) { 580 | this._buffer = []; 581 | job.open(function(err) { 582 | if (err) { 583 | batch.emit("error", err); 584 | return; 585 | } 586 | bstream._buffer.forEach(function(data) { 587 | bstream.write(data); 588 | }); 589 | if (bstream._ending) { 590 | bstream.end(); 591 | } 592 | bstream._buffer = []; 593 | }); 594 | } 595 | this._buffer.push(data); 596 | }; 597 | 598 | /*--------------------------------------------*/ 599 | 600 | /** 601 | * Class for Bulk API 602 | * 603 | * @class 604 | * @param {Connection} conn - Connection object 605 | */ 606 | var Bulk = function(conn) { 607 | this._conn = conn; 608 | this._logger = conn._logger; 609 | }; 610 | 611 | /** 612 | * Polling interval in milliseconds 613 | * @type {Number} 614 | */ 615 | Bulk.prototype.pollInterval = 1000; 616 | 617 | /** 618 | * Polling timeout in milliseconds 619 | * @type {Number} 620 | */ 621 | Bulk.prototype.pollTimeout = 10000; 622 | 623 | /** @private **/ 624 | Bulk.prototype._request = function(params, callback) { 625 | var conn = this._conn; 626 | params = _.clone(params); 627 | var baseUrl = [ conn.instanceUrl, "services/async", conn.version ].join('/'); 628 | params.url = baseUrl + params.path; 629 | var options = { 630 | responseContentType: params.responseType, 631 | beforesend: function(conn, params) { 632 | params.headers["X-SFDC-SESSION"] = conn.accessToken; 633 | }, 634 | parseError: function(err) { 635 | return { 636 | code: err.error.exceptionCode, 637 | message: err.error.exceptionMessage 638 | }; 639 | } 640 | }; 641 | delete params.path; 642 | delete params.responseType; 643 | return this._conn._request(params, callback, options); 644 | }; 645 | 646 | /** 647 | * Create and start bulkload job and batch 648 | * 649 | * @param {String} type - SObject type 650 | * @param {String} operation - Bulk load operation ('insert', 'update', 'upsert', 'delete', or 'hardDelete') 651 | * @param {Object} [options] - Options for bulk loading operation 652 | * @param {String} [options.extIdField] - External ID field name (used when upsert operation). 653 | * @param {Array.|stream.Stream|String} [input] - Input source for bulkload. Accepts array of records, CSv string, and CSV data input stream. 654 | * @param {Callback.>} [callback] - Callback function 655 | * @returns {Bulk~Batch} 656 | */ 657 | Bulk.prototype.load = function(type, operation, options, input, callback) { 658 | var self = this; 659 | if (!type || !operation) { 660 | throw new Error("Insufficient arguments. At least, 'type' and 'operation' are required."); 661 | } 662 | if (operation.toLowerCase() !== 'upsert') { // options is only for upsert operation 663 | callback = input; 664 | input = options; 665 | options = null; 666 | } 667 | var job = this.createJob(type, operation, options); 668 | var batch = job.createBatch(); 669 | var cleanup = function() { job.close(); }; 670 | batch.on('response', cleanup); 671 | batch.on('error', cleanup); 672 | batch.on('queue', function() { batch.poll(self.pollInterval, self.pollTimeout); }); 673 | return batch.execute(input, callback); 674 | }; 675 | 676 | 677 | /** 678 | * Create a new job instance 679 | * 680 | * @param {String} type - SObject type 681 | * @param {String} operation - Bulk load operation ('insert', 'update', 'upsert', 'delete', or 'hardDelete') 682 | * @param {Object} [options] - Options for bulk loading operation 683 | * @returns {Bulk~Job} 684 | */ 685 | Bulk.prototype.createJob = function(type, operation, options) { 686 | var job = new Job(this, type, operation, options); 687 | job.open(); 688 | return job; 689 | }; 690 | 691 | /** 692 | * Get a job instance specified by given job ID 693 | * 694 | * @param {String} jobId - Job ID 695 | * @returns {Bulk~Job} 696 | */ 697 | Bulk.prototype.job = function(jobId) { 698 | return new Job(this, null, null, null, jobId); 699 | }; 700 | 701 | 702 | /*--------------------------------------------*/ 703 | 704 | module.exports = Bulk; -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages asynchronous method response cache 3 | * @author Shinichi Tomita 4 | */ 5 | var events = require('events'), 6 | util = require('util'), 7 | _ = require('underscore')._; 8 | 9 | /** 10 | * Class for managing cache entry 11 | * 12 | * @private 13 | * @class 14 | * @constructor 15 | * @template T 16 | */ 17 | var CacheEntry = function() { 18 | this.fetching = false; 19 | }; 20 | 21 | util.inherits(CacheEntry, events.EventEmitter); 22 | 23 | /** 24 | * Get value in the cache entry 25 | * 26 | * @param {Callback.} [callback] - Callback function callbacked the cache entry updated 27 | * @returns {T|undefined} 28 | */ 29 | CacheEntry.prototype.get = function(callback) { 30 | if (!callback) { 31 | return this._value; 32 | } else { 33 | this.once('value', callback); 34 | if (!_.isUndefined(this._value)) { 35 | this.emit('value', this._value); 36 | } 37 | } 38 | }; 39 | 40 | /** 41 | * Set value in the cache entry 42 | * 43 | * @param {T} [value] - A value for caching 44 | */ 45 | CacheEntry.prototype.set = function(value) { 46 | this._value = value; 47 | this.emit('value', this._value); 48 | }; 49 | 50 | /** 51 | * Clear cached value 52 | */ 53 | CacheEntry.prototype.clear = function() { 54 | this.fetching = false; 55 | delete this._value; 56 | }; 57 | 58 | 59 | /** 60 | * Caching manager for async methods 61 | * 62 | * @class 63 | * @constructor 64 | */ 65 | var Cache = function() { 66 | this._entries = {}; 67 | }; 68 | 69 | /** 70 | * retrive cache entry, or create if not exists. 71 | * 72 | * @param {String} [key] - Key of cache entry 73 | * @returns {CacheEntry} 74 | */ 75 | Cache.prototype.get = function(key) { 76 | if (key && this._entries[key]) { 77 | return this._entries[key]; 78 | } else { 79 | var entry = new CacheEntry(); 80 | this._entries[key] = entry; 81 | return entry; 82 | } 83 | }; 84 | 85 | /** 86 | * clear cache entries prefix matching given key 87 | * @param {String} [key] - Key prefix of cache entry to clear 88 | */ 89 | Cache.prototype.clear = function(key) { 90 | for (var k in this._entries) { 91 | if (!key || k.indexOf(key) === 0) { 92 | this._entries[k].clear(); 93 | } 94 | } 95 | }; 96 | 97 | /** 98 | * create and return cache key from namespace and serialized arguments. 99 | * @private 100 | */ 101 | function createCacheKey(namespace, args) { 102 | args = Array.prototype.slice.apply(args); 103 | return namespace + '(' + _.map(args, function(a){ return JSON.stringify(a); }).join(',') + ')'; 104 | } 105 | 106 | /** 107 | * Enable caching for async call fn to intercept the response and store it to cache. 108 | * The original async calll fn is always invoked. 109 | * 110 | * @protected 111 | * @param {Function} fn - Function to covert cacheable 112 | * @param {Object} [scope] - Scope of function call 113 | * @param {Object} [options] - Options 114 | * @return {Function} - Cached version of function 115 | */ 116 | Cache.prototype.makeResponseCacheable = function(fn, scope, options) { 117 | var cache = this; 118 | options = options || {}; 119 | return function() { 120 | var args = Array.prototype.slice.apply(arguments); 121 | var callback = args.pop(); 122 | if (!_.isFunction(callback)) { 123 | args.push(callback); 124 | callback = null; 125 | } 126 | var key = _.isString(options.key) ? options.key : 127 | _.isFunction(options.key) ? options.key.apply(scope, args) : 128 | createCacheKey(options.namespace, args); 129 | var entry = cache.get(key); 130 | entry.fetching = true; 131 | if (callback) { 132 | args.push(function(err, result) { 133 | entry.set({ error: err, result: result }); 134 | callback(err, result); 135 | }); 136 | } 137 | var ret, error; 138 | try { 139 | ret = fn.apply(scope || this, args); 140 | } catch(e) { 141 | error = e; 142 | } 143 | if (ret && _.isFunction(ret.then)) { // if the returned value is promise 144 | if (!callback) { 145 | return ret.then(function(result) { 146 | entry.set({ error: undefined, result: result }); 147 | return result; 148 | }, function(err) { 149 | entry.set({ error: err, result: undefined }); 150 | throw err; 151 | }); 152 | } else { 153 | return ret; 154 | } 155 | } else { 156 | entry.set({ error: error, result: ret }); 157 | if (error) { throw error; } 158 | return ret; 159 | } 160 | }; 161 | }; 162 | 163 | /** 164 | * Enable caching for async call fn to lookup the response cache first, then invoke original if no cached value. 165 | * 166 | * @protected 167 | * @param {Function} fn - Function to covert cacheable 168 | * @param {Object} [scope] - Scope of function call 169 | * @param {Object} [options] - Options 170 | * @return {Function} - Cached version of function 171 | */ 172 | Cache.prototype.makeCacheable = function(fn, scope, options) { 173 | var cache = this; 174 | options = options || {}; 175 | var $fn = function() { 176 | var args = Array.prototype.slice.apply(arguments); 177 | var callback = args.pop(); 178 | if (!_.isFunction(callback)) { 179 | args.push(callback); 180 | } 181 | var key = _.isString(options.key) ? options.key : 182 | _.isFunction(options.key) ? options.key.apply(scope, args) : 183 | createCacheKey(options.namespace, args); 184 | var entry = cache.get(key); 185 | if (!_.isFunction(callback)) { // if callback is not given in last arg, return cached result (immediate). 186 | var value = entry.get(); 187 | if (!value) { throw new Error('Function call result is not cached yet.'); } 188 | if (value.error) { throw value.error; } 189 | return value.result; 190 | } 191 | entry.get(function(value) { 192 | callback(value.error, value.result); 193 | }); 194 | if (!entry.fetching) { // only when no other client is calling function 195 | entry.fetching = true; 196 | args.push(function(err, result) { 197 | entry.set({ error: err, result: result }); 198 | }); 199 | fn.apply(scope || this, args); 200 | } 201 | }; 202 | $fn.clear = function() { 203 | var key = _.isString(options.key) ? options.key : 204 | _.isFunction(options.key) ? options.key.apply(scope, arguments) : 205 | createCacheKey(options.namespace, arguments); 206 | cache.clear(key); 207 | }; 208 | return $fn; 209 | }; 210 | 211 | 212 | module.exports = Cache; 213 | -------------------------------------------------------------------------------- /lib/chatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages Salesforce Chatter REST API calls 3 | * @author Shinichi Tomita 4 | */ 5 | 6 | var util = require('util'), 7 | _ = require('underscore'), 8 | Promise = require('./promise'); 9 | 10 | /** 11 | * API class for Chatter REST API call 12 | * 13 | * @class 14 | * @param {Connection} conn Connection 15 | */ 16 | var Chatter = module.exports = function(conn) { 17 | this._conn = conn; 18 | }; 19 | 20 | /** 21 | * Sending request to API endpoint 22 | * @private 23 | */ 24 | Chatter.prototype._request = function(params, callback) { 25 | if (/^(put|post|patch)$/i.test(params.method) && params.body) { 26 | params.headers = { 27 | "Content-Type": "application/json" 28 | }; 29 | params.body = JSON.stringify(params.body); 30 | } 31 | params.url = this._normalizeUrl(params.url); 32 | return this._conn._request(params, callback); 33 | }; 34 | 35 | /** 36 | * Convert path to site root relative url 37 | * @private 38 | */ 39 | Chatter.prototype._normalizeUrl = function(url) { 40 | if (url.indexOf('/chatter/') === 0 || url.indexOf('/connect/') === 0) { 41 | return '/services/data/v' + this._conn.version + url; 42 | } else if (/^\/v[\d]+\.[\d]+\//.test(url)) { 43 | return '/services/data' + url; 44 | } else if (url.indexOf('/services/') !== 0 && url[0] === '/') { 45 | return '/services/data/v' + this._conn.version + '/chatter' + url; 46 | } else { 47 | return url; 48 | } 49 | }; 50 | 51 | /** 52 | * @typedef {Object} Chatter~RequestParams 53 | * @prop {String} method - HTTP method 54 | * @prop {String} url - Resource URL 55 | * @prop {String} [body] - HTTP body (in POST/PUT/PATCH methods) 56 | */ 57 | 58 | /** 59 | * @typedef {Object} Chatter~RequestResult 60 | */ 61 | 62 | /** 63 | * Make a request for chatter API resource 64 | * 65 | * @param {Chatter~RequestParams} params - Paramters representing HTTP request 66 | * @param {Callback.} [callback] - Callback func 67 | * @returns {Chatter~Request} 68 | */ 69 | Chatter.prototype.request = function(params, callback) { 70 | return new Request(this, params).thenCall(callback); 71 | }; 72 | 73 | /** 74 | * Make a resource request to chatter API 75 | * 76 | * @param {String} url - Resource URL 77 | * @param {Object} [queryParams] - Query parameters (in hash object) 78 | * @returns {Chatter~Resource} 79 | */ 80 | Chatter.prototype.resource = function(url, queryParams) { 81 | return new Resource(this, url, queryParams); 82 | }; 83 | 84 | /** 85 | * @typedef {Object} Chatter~BatchRequestResult 86 | * @prop {Boolean} hasError - Flag if the batch has one or more errors 87 | * @prop {Array.} results - Batch request results in array 88 | * @prop {Number} results.statusCode - HTTP response status code 89 | * @prop {Chatter~RequestResult} results.result - Parsed HTTP response body 90 | */ 91 | 92 | /** 93 | * Make a batch request to chatter API 94 | * 95 | * @params {Array.} requests - Chatter API requests 96 | * @param {Callback.} [callback] - Callback func 97 | * @returns {Promise.} 98 | */ 99 | Chatter.prototype.batch = function(requests, callback) { 100 | var self = this; 101 | var batchRequests = [], batchDeferreds = []; 102 | _.forEach(requests, function(request) { 103 | var deferred = Promise.defer(); 104 | request._promise = deferred.promise; 105 | batchRequests.push(request.batchParams()); 106 | batchDeferreds.push(deferred); 107 | }); 108 | var params = { 109 | method: 'POST', 110 | url: this._normalizeUrl('/connect/batch'), 111 | body: { 112 | batchRequests: batchRequests 113 | } 114 | }; 115 | return this._request(params).then(function(res) { 116 | _.forEach(res.results, function(result, i) { 117 | var deferred = batchDeferreds[i]; 118 | if (result.statusCode >= 400) { 119 | deferred.reject(result.result); 120 | } else { 121 | deferred.resolve(result.result); 122 | } 123 | }); 124 | return res; 125 | }).thenCall(callback); 126 | }; 127 | 128 | /*--------------------------------------------*/ 129 | /** 130 | * A class representing chatter API request 131 | * 132 | * @protected 133 | * @class Chatter~Request 134 | * @implements {Promise.} 135 | * @param {Chatter} chatter - Chatter API object 136 | * @param {Chatter~RequestParams} params - Paramters representing HTTP request 137 | */ 138 | var Request = function(chatter, params) { 139 | this._chatter = chatter; 140 | this._params = params; 141 | this._promise = null; 142 | }; 143 | 144 | /** 145 | * @typedef {Object} Chatter~BatchRequestParams 146 | * @prop {String} method - HTTP method 147 | * @prop {String} url - Resource URL 148 | * @prop {String} [richInput] - HTTP body (in POST/PUT/PATCH methods) 149 | */ 150 | 151 | /** 152 | * Retrieve parameters in batch request form 153 | * 154 | * @method Chatter~Request#batchParams 155 | * @returns {Chatter~BatchRequestParams} 156 | */ 157 | Request.prototype.batchParams = function() { 158 | var params = this._params; 159 | var batchParams = { 160 | method: params.method, 161 | url: this._chatter._normalizeUrl(params.url) 162 | }; 163 | if (this._params.body) { 164 | batchParams.richInput = this._params.body; 165 | } 166 | return batchParams; 167 | }; 168 | 169 | /** 170 | * Retrieve parameters in batch request form 171 | * 172 | * @method Chatter~Request#promise 173 | * @returns {Promise.} 174 | */ 175 | Request.prototype.promise = function() { 176 | return this._promise || this._chatter._request(this._params); 177 | }; 178 | 179 | /** 180 | * Returns Node.js Stream object for request 181 | * 182 | * @method Chatter~Request#stream 183 | * @returns {stream.Stream} 184 | */ 185 | Request.prototype.stream = function() { 186 | return this._chatter._request(this._params).stream(); 187 | }; 188 | 189 | /** 190 | * Promise/A+ interface 191 | * http://promises-aplus.github.io/promises-spec/ 192 | * 193 | * Delegate to deferred promise, return promise instance for batch result 194 | * 195 | * @method Chatter~Request#then 196 | */ 197 | Request.prototype.then = function(onResolve, onReject) { 198 | return this.promise().then(onResolve, onReject); 199 | }; 200 | 201 | /** 202 | * Promise/A+ extension 203 | * Call "then" using given node-style callback function 204 | * 205 | * @method Chatter~Request#thenCall 206 | */ 207 | Request.prototype.thenCall = function(callback) { 208 | return _.isFunction(callback) ? this.promise().thenCall(callback) : this; 209 | }; 210 | 211 | 212 | /*--------------------------------------------*/ 213 | /** 214 | * A class representing chatter API resource 215 | * 216 | * @protected 217 | * @class Chatter~Resource 218 | * @extends Chatter~Request 219 | * @param {Chatter} chatter - Chatter API object 220 | * @param {String} url - Resource URL 221 | * @param {Object} [queryParams] - Query parameters (in hash object) 222 | */ 223 | var Resource = function(chatter, url, queryParams) { 224 | if (queryParams) { 225 | var qstring = _.map(_.keys(queryParams), function(name) { 226 | return name + "=" + encodeURIComponent(queryParams[name]); 227 | }).join('&'); 228 | url += (url.indexOf('?') > 0 ? '&' : '?') + qstring; 229 | } 230 | Resource.super_.call(this, chatter, { method: 'GET', url: url }); 231 | this._url = url; 232 | }; 233 | 234 | util.inherits(Resource, Request); 235 | 236 | /** 237 | * Create a new resource 238 | * 239 | * @method Chatter~Resource#create 240 | * @param {Object} data - Data to newly post 241 | * @param {Callback.} [callback] - Callback function 242 | * @returns {Chatter~Request} 243 | */ 244 | Resource.prototype.create = function(data, callback) { 245 | return this._chatter.request({ 246 | method: 'POST', 247 | url: this._url, 248 | body: data 249 | }).thenCall(callback); 250 | }; 251 | 252 | /** 253 | * Retrieve resource content 254 | * 255 | * @method Chatter~Resource#retrieve 256 | * @param {Callback.} [callback] - Callback function 257 | * @returns {Chatter~Request} 258 | */ 259 | Resource.prototype.retrieve = function(callback) { 260 | return this.thenCall(callback); 261 | }; 262 | 263 | /** 264 | * Update specified resource 265 | * 266 | * @method Chatter~Resource#update 267 | * @param {Obejct} data - Data to update 268 | * @param {Callback.} [callback] - Callback function 269 | * @returns {Chatter~Request} 270 | */ 271 | Resource.prototype.update = function(data, callback) { 272 | return this._chatter.request({ 273 | method: 'POST', 274 | url: this._url, 275 | body: data 276 | }).thenCall(callback); 277 | }; 278 | 279 | /** 280 | * Synonym of Resource#delete() 281 | * 282 | * @method Chatter~Resource#del 283 | * @param {Callback.} [callback] - Callback function 284 | * @returns {Chatter~Request} 285 | */ 286 | /** 287 | * Delete specified resource 288 | * 289 | * @method Chatter~Resource#delete 290 | * @param {Callback.} [callback] - Callback function 291 | * @returns {Chatter~Request} 292 | */ 293 | Resource.prototype.del = 294 | Resource.prototype["delete"] = function(callback) { 295 | return this._chatter.request({ 296 | method: 'DELETE', 297 | url: this._url 298 | }).thenCall(callback); 299 | }; 300 | -------------------------------------------------------------------------------- /lib/csv.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | SfDate = require('./date'); 3 | 4 | /** 5 | * @private 6 | */ 7 | function toCSV(records, headers, options) { 8 | options = options || {}; 9 | if (!headers) { 10 | headers = extractHeaders(records, options); 11 | } 12 | var rows = _.map(records, function(record){ return recordToCSV(record, headers); }); 13 | return arrayToCSV(headers) + "\n" + rows.join("\n"); 14 | } 15 | 16 | /** 17 | * @private 18 | */ 19 | function extractHeaders(records, options) { 20 | options = options || {}; 21 | var headers = {}; 22 | _.forEach(records, function(record) { 23 | for (var key in record) { 24 | var value = record[key]; 25 | if (record.hasOwnProperty(key) && (value === null || typeof value !== 'object')) { 26 | headers[key] = true; 27 | } 28 | } 29 | }); 30 | return _.keys(headers); 31 | } 32 | 33 | /** 34 | * @private 35 | */ 36 | function recordToCSV(record, headers) { 37 | var row = []; 38 | _.forEach(headers, function(header) { 39 | var value = record[header]; 40 | if (typeof value === 'undefined') { value = null; } 41 | row.push(value); 42 | }); 43 | return arrayToCSV(row); 44 | } 45 | 46 | /** 47 | * @private 48 | */ 49 | function arrayToCSV(arr) { 50 | return _.map(arr, escapeCSV).join(','); 51 | } 52 | 53 | /** 54 | * @private 55 | */ 56 | function escapeCSV(str) { 57 | if (str === null || typeof str === 'undefined') { str = ''; } 58 | str = String(str); 59 | if (str.indexOf('"') >= 0 || str.indexOf(',') >= 0 || /[\n\r]/.test(str)) { 60 | str = '"' + str.replace(/"/g, '""') + '"'; 61 | } 62 | return str; 63 | } 64 | 65 | 66 | 67 | /** 68 | * @private 69 | * @class 70 | * @constructor 71 | * @param {String} text - CSV string 72 | */ 73 | var CSVParser = function(text) { 74 | this.text = text; 75 | this.cursor = 0; 76 | }; 77 | 78 | CSVParser.prototype = { 79 | 80 | nextToken : function() { 81 | var cell; 82 | var dquoted = false; 83 | var firstChar = this.text.charAt(this.cursor); 84 | if (firstChar === '' || firstChar === '\r' || firstChar === '\n') { 85 | return null; 86 | } 87 | if (firstChar === '"') { 88 | dquoted = true; 89 | } 90 | if (dquoted) { 91 | var dq = this.cursor; 92 | while(true) { 93 | dq++; 94 | dq = this.text.indexOf('"', dq); 95 | if (dq<0 || this.text.charAt(dq+1) !== '"') { 96 | break; 97 | } else { 98 | dq++; 99 | } 100 | } 101 | if (dq>=0) { 102 | var delim = this.text.charAt(dq+1); 103 | cell = this.text.substring(this.cursor, dq+1); 104 | this.cursor = dq + (delim === ',' ? 2 : 1); 105 | } else { 106 | cell = this.text.substring(this.cursor); 107 | this.cursor = this.text.length; 108 | } 109 | return cell.replace(/""/g,'"').replace(/^"/,'').replace(/"$/,''); 110 | } else { 111 | var comma = this.text.indexOf(',', this.cursor); 112 | var cr = this.text.indexOf('\r', this.cursor); 113 | var lf = this.text.indexOf('\n', this.cursor); 114 | comma = comma<0 ? this.text.length+1 : comma; 115 | cr = cr<0 ? this.text.length+1 : cr; 116 | lf = lf<0 ? this.text.length+1 : lf; 117 | var pivot = Math.min(comma, cr, lf, this.text.length); 118 | cell = this.text.substring(this.cursor, pivot); 119 | this.cursor = pivot; 120 | if (comma === pivot) { 121 | this.cursor++; 122 | } 123 | return cell; 124 | } 125 | }, 126 | 127 | nextLine : function() { 128 | for (var c = this.text.charAt(this.cursor); 129 | c === '\r' || c === '\n'; 130 | c = this.text.charAt(++this.cursor)) 131 | {} 132 | return this.cursor !== this.text.length; 133 | } 134 | 135 | }; 136 | 137 | /** 138 | * @private 139 | */ 140 | function parseCSV(str) { 141 | var parser = new CSVParser(str); 142 | var headers = []; 143 | var token; 144 | if (parser.nextLine()) { 145 | token = parser.nextToken(); 146 | while (!_.isUndefined(token) && !_.isNull(token)) { 147 | headers.push(token); 148 | token = parser.nextToken(); 149 | } 150 | } 151 | var rows = []; 152 | while (parser.nextLine()) { 153 | var row = {}; 154 | token = parser.nextToken(); 155 | var i = 0; 156 | while (!_.isUndefined(token) && !_.isNull(token)) { 157 | var header = headers[i++]; 158 | row[header] = token; 159 | token = parser.nextToken(); 160 | } 161 | rows.push(row); 162 | } 163 | return rows; 164 | } 165 | 166 | 167 | /** 168 | * @protected 169 | */ 170 | module.exports = { 171 | toCSV : toCSV, 172 | extractHeaders : extractHeaders, 173 | recordToCSV : recordToCSV, 174 | arrayToCSV : arrayToCSV, 175 | parseCSV : parseCSV 176 | }; 177 | -------------------------------------------------------------------------------- /lib/date.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore")._; 2 | 3 | /** 4 | * A date object to keep Salesforce date literal 5 | * 6 | * @class 7 | * @constructor 8 | * @see http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_dateformats.htm 9 | */ 10 | var SfDate = module.exports = function(literal) { 11 | this._literal = literal; 12 | }; 13 | 14 | /** 15 | * Returns literal when converted to string 16 | * 17 | * @override 18 | */ 19 | SfDate.prototype.toString = 20 | SfDate.prototype.toJSON = function() { return this._literal; }; 21 | 22 | 23 | /** @private **/ 24 | function zeropad(n) { return (n<10 ? "0" : "") + n; } 25 | 26 | /** 27 | * Convert JavaScript date object to ISO8601 Date format (e.g. 2012-10-31) 28 | * 29 | * @param {String|Number|Date} date - Input date 30 | * @returns {SfDate} - Salesforce date literal with ISO8601 date format 31 | */ 32 | SfDate.toDateLiteral = function(date) { 33 | if (_.isNumber(date)) { 34 | date = new Date(date); 35 | } else if (_.isString(date)) { 36 | date = SfDate.parseDate(date); 37 | } 38 | var yy = date.getFullYear(); 39 | var mm = date.getMonth()+1; 40 | var dd = date.getDate(); 41 | var dstr = [ yy, zeropad(mm), zeropad(dd) ].join("-"); 42 | return new SfDate(dstr); 43 | }; 44 | 45 | /** 46 | * Convert JavaScript date object to ISO8601 DateTime format 47 | * (e.g. 2012-10-31T12:34:56Z) 48 | * 49 | * @param {String|Number|Date} date - Input date 50 | * @returns {SfDate} - Salesforce date literal with ISO8601 datetime format 51 | */ 52 | SfDate.toDateTimeLiteral = function(date) { 53 | if (_.isNumber(date)) { 54 | date = new Date(date); 55 | } else if (_.isString(date)) { 56 | date = SfDate.parseDate(date); 57 | } 58 | var yy = date.getUTCFullYear(); 59 | var mm = date.getUTCMonth()+1; 60 | var dd = date.getUTCDate(); 61 | var hh = date.getUTCHours(); 62 | var mi = date.getUTCMinutes(); 63 | var ss = date.getUTCSeconds(); 64 | var dtstr = 65 | [ yy, zeropad(mm), zeropad(dd) ].join("-") + "T" + 66 | [ zeropad(hh), zeropad(mi), zeropad(ss) ].join(":") + "Z"; 67 | return new SfDate(dtstr); 68 | }; 69 | 70 | /** 71 | * Parse IS08601 date(time) formatted string and return date instance 72 | * 73 | * @param {String} str 74 | * @returns {Date} 75 | */ 76 | SfDate.parseDate = function(str) { 77 | var d = new Date(); 78 | var regexp = /^([\d]{4})-?([\d]{2})-?([\d]{2})(T([\d]{2}):?([\d]{2}):?([\d]{2})(.([\d]{3}))?(Z|([\+\-])([\d]{2}):?([\d]{2})))?$/; 79 | var m = str.match(regexp); 80 | if (m) { 81 | d = new Date(0); 82 | if (!m[4]) { 83 | d.setFullYear(parseInt(m[1], 10)); 84 | d.setDate(parseInt(m[3], 10)); 85 | d.setMonth(parseInt(m[2], 10) - 1); 86 | d.setHours(0); 87 | d.setMinutes(0); 88 | d.setSeconds(0); 89 | d.setMilliseconds(0); 90 | } else { 91 | d.setUTCFullYear(parseInt(m[1], 10)); 92 | d.setUTCDate(parseInt(m[3], 10)); 93 | d.setUTCMonth(parseInt(m[2], 10) - 1); 94 | d.setUTCHours(parseInt(m[5], 10)); 95 | d.setUTCMinutes(parseInt(m[6], 10)); 96 | d.setUTCSeconds(parseInt(m[7], 10)); 97 | d.setUTCMilliseconds(parseInt(m[9] || '0', 10)); 98 | if (m[10] && m[10] !== 'Z') { 99 | var offset = parseInt(m[12],10) * 60 + parseInt(m[13], 10); 100 | d.setTime((m[11] === '+' ? -1 : 1) * offset * 60 * 1000 +d.getTime()); 101 | } 102 | } 103 | return d; 104 | } else { 105 | throw new Error("Invalid date format is specified : " + str); 106 | } 107 | }; 108 | 109 | /* 110 | * Pre-defined Salesforce Date Literals 111 | */ 112 | var SfDateLiterals = { 113 | YESTERDAY: 1, 114 | TODAY: 1, 115 | TOMORROW: 1, 116 | LAST_WEEK: 1, 117 | THIS_WEEK: 1, 118 | NEXT_WEEK: 1, 119 | LAST_MONTH: 1, 120 | THIS_MONTH: 1, 121 | NEXT_MONTH: 1, 122 | LAST_90_DAYS: 1, 123 | NEXT_90_DAYS: 1, 124 | LAST_N_DAYS: 2, 125 | NEXT_N_DAYS: 2, 126 | THIS_QUARTER: 1, 127 | LAST_QUARTER: 1, 128 | NEXT_QUARTER: 1, 129 | NEXT_N_QUARTERS: 2, 130 | LAST_N_QUARTERS: 2, 131 | THIS_YEAR: 1, 132 | LAST_YEAR: 1, 133 | NEXT_YEAR: 1, 134 | NEXT_N_YEARS: 2, 135 | LAST_N_YEARS: 2, 136 | THIS_FISCAL_QUARTER: 1, 137 | LAST_FISCAL_QUARTER: 1, 138 | NEXT_FISCAL_QUARTER: 1, 139 | NEXT_N_FISCAL_QUARTERS:2, 140 | LAST_N_FISCAL_QUARTERS:2, 141 | THIS_FISCAL_YEAR:1, 142 | LAST_FISCAL_YEAR:1, 143 | NEXT_FISCAL_YEAR:1, 144 | NEXT_N_FISCAL_YEARS: 2, 145 | LAST_N_FISCAL_YEARS: 2 146 | }; 147 | 148 | for (var literal in SfDateLiterals) { 149 | var type = SfDateLiterals[literal]; 150 | SfDate[literal] = 151 | type === 1 ? new SfDate(literal) : createLiteralBuilder(literal); 152 | } 153 | 154 | /** @private **/ 155 | function createLiteralBuilder(literal) { 156 | return function(num) { return new SfDate(literal + ":" + num); }; 157 | } 158 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @protected 3 | * @class 4 | * @constructor 5 | * @param {String|Number} logLevel - Log level 6 | */ 7 | var Logger = module.exports = function(logLevel) { 8 | if (typeof logLevel === 'string') { 9 | logLevel = LogLevels[logLevel]; 10 | } 11 | if (!logLevel) { 12 | logLevel = LogLevels.INFO; 13 | } 14 | this._logLevel = logLevel; 15 | }; 16 | 17 | /** 18 | * @memberof Logger 19 | */ 20 | var LogLevels = Logger.LogLevels = { 21 | "DEBUG" : 1, 22 | "INFO" : 2, 23 | "WARN" : 3, 24 | "ERROR" : 4, 25 | "FATAL" : 5 26 | }; 27 | 28 | /** 29 | * Output log 30 | * 31 | * @param {String} level - Logging target level 32 | * @param {String} message - Message to log 33 | */ 34 | Logger.prototype.log = function(level, message) { 35 | if (this._logLevel <= level) { 36 | if (level < LogLevels.ERROR) { 37 | console.log(message); 38 | } else { 39 | console.error(message); 40 | } 41 | } 42 | }; 43 | 44 | for (var level in LogLevels) { 45 | Logger.prototype[level.toLowerCase()] = createLoggerFunction(LogLevels[level]); 46 | } 47 | 48 | function createLoggerFunction(level) { 49 | return function(message) { this.log(level, message); }; 50 | } 51 | -------------------------------------------------------------------------------- /lib/oauth2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages Salesforce OAuth2 operations 3 | * @author Shinichi Tomita 4 | */ 5 | var querystring = require('querystring'), 6 | request = require('./request'), 7 | _ = require('underscore')._; 8 | 9 | /** 10 | * @private 11 | */ 12 | function postParams(url, params, callback) { 13 | return request({ 14 | method : 'POST', 15 | url : url, 16 | body : querystring.stringify(params), 17 | headers : { 18 | "content-type" : "application/x-www-form-urlencoded" 19 | } 20 | }).then(function(response) { 21 | var res = JSON.parse(response.body); 22 | if (response.statusCode >= 400) { 23 | var err = new Error(res.error + ": " + res.error_description); 24 | err.name = res.error; 25 | throw err; 26 | } 27 | return res; 28 | }).thenCall(callback); 29 | } 30 | 31 | var defaults = { 32 | loginUrl : "https://login.salesforce.com" 33 | }; 34 | 35 | /** 36 | * OAuth2 class 37 | * 38 | * @class 39 | * @constructor 40 | * @param {Object} options - OAuth2 config options 41 | * @param {String} [options.loginUrl] - Salesforce login server URL 42 | * @param {String} [options.authzServiceUrl] - OAuth2 authorization service URL. If not specified, it generates from default by adding to login server URL. 43 | * @param {String} [options.tokenServiceUrl] - OAuth2 token service URL. If not specified it generates from default by adding to login server URL. 44 | * @param {String} options.clientId - OAuth2 client ID. 45 | * @param {String} options.clientSecret - OAuth2 client secret. 46 | * @param {String} options.redirectUri - URI to be callbacked from Salesforce OAuth2 authorization service. 47 | */ 48 | var OAuth2 = module.exports = function(options) { 49 | if (options.authzServiceUrl && options.tokenServiceUrl) { 50 | this.loginUrl = options.authzServiceUrl.split('/').slice(0, 3).join('/'); 51 | this.authzServiceUrl = options.authzServiceUrl; 52 | this.tokenServiceUrl = options.tokenServiceUrl; 53 | } else { 54 | this.loginUrl = options.loginUrl || defaults.loginUrl; 55 | this.authzServiceUrl = this.loginUrl + "/services/oauth2/authorize"; 56 | this.tokenServiceUrl = this.loginUrl + "/services/oauth2/token"; 57 | } 58 | this.clientId = options.clientId; 59 | this.clientSecret = options.clientSecret; 60 | this.redirectUri = options.redirectUri; 61 | }; 62 | 63 | 64 | 65 | /** 66 | * 67 | */ 68 | _.extend(OAuth2.prototype, /** @lends OAuth2.prototype **/ { 69 | 70 | /** 71 | * Get Salesforce OAuth2 authorization page URL to redirect user agent. 72 | * 73 | * @param {Object} params - Parameters 74 | * @param {String} params.scope - Scope values in space-separated string 75 | * @param {String} params.state - State parameter 76 | * @returns {String} Authorization page URL 77 | */ 78 | getAuthorizationUrl : function(params) { 79 | params = _.extend({ 80 | response_type : "code", 81 | client_id : this.clientId, 82 | redirect_uri : this.redirectUri 83 | }, params || {}); 84 | return this.authzServiceUrl + 85 | (this.authzServiceUrl.indexOf('?') >= 0 ? "&" : "?") + 86 | querystring.stringify(params); 87 | }, 88 | 89 | /** 90 | * @typedef TokenResponse 91 | * @type {Object} 92 | * @property {String} access_token 93 | * @property {String} refresh_token 94 | */ 95 | 96 | /** 97 | * OAuth2 Refresh Token Flow 98 | * 99 | * @param {String} refreshToken - Refresh token 100 | * @param {Callback.} [callback] - Callback function 101 | * @returns {Promise.} 102 | */ 103 | refreshToken : function(refreshToken, callback) { 104 | return postParams(this.tokenServiceUrl, { 105 | grant_type : "refresh_token", 106 | refresh_token : refreshToken, 107 | client_id : this.clientId, 108 | client_secret : this.clientSecret 109 | }, callback); 110 | }, 111 | 112 | /** 113 | * OAuth2 Web Server Authentication Flow (Authorization Code) 114 | * Access Token Request 115 | * 116 | * @param {String} code - Authorization code 117 | * @param {Callback.} [callback] - Callback function 118 | * @returns {Promise.} 119 | */ 120 | requestToken : function(code, callback) { 121 | return postParams(this.tokenServiceUrl, { 122 | grant_type : "authorization_code", 123 | code : code, 124 | client_id : this.clientId, 125 | client_secret : this.clientSecret, 126 | redirect_uri : this.redirectUri 127 | }, callback); 128 | }, 129 | 130 | /** 131 | * OAuth2 Username-Password Flow (Resource Owner Password Credentials) 132 | * 133 | * @param {String} username - Salesforce username 134 | * @param {String} password - Salesforce password 135 | * @param {Callback.} [callback] - Callback function 136 | * @returns {Promise.} 137 | */ 138 | authenticate : function(username, password, callback) { 139 | return postParams(this.tokenServiceUrl, { 140 | grant_type : "password", 141 | username : username, 142 | password : password, 143 | client_id : this.clientId, 144 | client_secret : this.clientSecret, 145 | redirect_uri : this.redirectUri 146 | }, callback); 147 | } 148 | }); 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /lib/promise.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | _ = require('underscore')._; 3 | 4 | /** 5 | * Promises/A+ spec compliant class, with a little extension 6 | * http://promises-aplus.github.io/promises-spec/ 7 | * 8 | * @class Promise 9 | * @constructor 10 | * @param {Promise.|T} o - Object to wrap with promise 11 | * @template T 12 | */ 13 | var Promise = function(o) { 14 | this._promise = Q(o); 15 | }; 16 | 17 | /** 18 | * @callback FulfilledCallback 19 | * @param {T} result - Fulfilled value 20 | * @returns {S} 21 | * @template T,S 22 | */ 23 | 24 | /** 25 | * @callback RejectedCallback 26 | * @param {Error} reason - Rejected reason 27 | * @returns {S} 28 | * @template S 29 | */ 30 | 31 | /** 32 | * The "then" method from the Promises/A+ specification 33 | * 34 | * @param {FulfilledCallback.} [onFulfilled] 35 | * @param {RejectedCallback.} [onRejected] 36 | * @returns {Promise.} 37 | */ 38 | Promise.prototype.then = function() { 39 | // Delegate Q promise implementation and wrap by our Promise instance 40 | return new Promise(this._promise.then.apply(this._promise, arguments)); 41 | }; 42 | 43 | /** 44 | * Call "then" using given node-style callback function 45 | * 46 | * @param {Callback.} [callback] - Callback function 47 | * @returns {Promise.} 48 | */ 49 | Promise.prototype.thenCall = function(callback) { 50 | return _.isFunction(callback) ? this.then(function(res) { 51 | return callback(null, res); 52 | }, function(err) { 53 | return callback(err); 54 | }) : this; 55 | }; 56 | 57 | /** 58 | * A sugar method, equivalent to promise.then(undefined, onRejected). 59 | * 60 | * @param {RejectedCallback.} onRejected 61 | * @returns {Promise.} 62 | */ 63 | Promise.prototype.fail = function() { 64 | return new Promise(this._promise.fail.apply(this._promise, arguments)); 65 | }; 66 | 67 | /** 68 | * Alias for completion 69 | * 70 | * @param {FulfilledCallback.} [onFulfilled] 71 | * @returns {Promise.} 72 | */ 73 | Promise.prototype.done = function() { 74 | return new Promise(this._promise.done.apply(this._promise, arguments)); 75 | }; 76 | 77 | /** 78 | * @param {...Promise.<*>} p 79 | */ 80 | Promise.when = function() { 81 | return new Promise(Q.when.apply(Q, arguments)); 82 | }; 83 | 84 | /** 85 | * Returns rejecting promise with given reason 86 | * 87 | * @param {Error} reason - Rejecting reason 88 | * @returns {Promise} 89 | */ 90 | Promise.reject = function(reason) { 91 | return new Promise(Q.reject(reason)); 92 | }; 93 | 94 | /** 95 | * Returns a promise that is fulfilled with an array containing the fulfillment value of each promise, 96 | * or is rejected with the same rejection reason as the first promise to be rejected. 97 | * 98 | * @param {Array.|*>} promises 99 | * @returns {Promise.>} 100 | */ 101 | Promise.all = function() { 102 | return new Promise(Q.all.apply(Q, arguments)); 103 | }; 104 | 105 | /** 106 | * Returns a deferred object 107 | * 108 | * @returns {Deferred} 109 | */ 110 | Promise.defer = function() { 111 | return new Deferred(); 112 | }; 113 | 114 | /** 115 | * Deferred object 116 | * 117 | * @protected 118 | * @constructor 119 | */ 120 | var Deferred = function() { 121 | this._deferred = Q.defer(); 122 | this.promise = new Promise(this._deferred.promise); 123 | }; 124 | 125 | /** 126 | * Resolve promise 127 | * @param {*} result - Resolving result 128 | */ 129 | Deferred.prototype.resolve = function() { 130 | return this._deferred.resolve.apply(this._promise, arguments); 131 | }; 132 | 133 | /** 134 | * Reject promise 135 | * @param {Error} error - Rejecting reason 136 | */ 137 | Deferred.prototype.reject = function() { 138 | return this._deferred.reject.apply(this._promise, arguments); 139 | }; 140 | 141 | /** 142 | * 143 | */ 144 | module.exports = Promise; 145 | -------------------------------------------------------------------------------- /lib/record-stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Represents stream that handles Salesforce record as stream data 3 | * @author Shinichi Tomita 4 | */ 5 | var events = require('events'), 6 | stream = require('stream'), 7 | Stream = stream.Stream, 8 | util = require('util'), 9 | _ = require('underscore'), 10 | CSV = require('./csv'); 11 | 12 | /** 13 | * Class for Record Stream 14 | * 15 | * @abstract 16 | * @class 17 | * @constructor 18 | * @extends events.EventEmitter 19 | */ 20 | var RecordStream = module.exports = function() { 21 | this.sendable = false; 22 | this.receivable = false; 23 | this.on('error', function() { 24 | this.sendable = false; 25 | this.recievable = false; 26 | }); 27 | this.on('end', function() { 28 | this.recievable = false; 29 | }); 30 | }; 31 | 32 | util.inherits(RecordStream, events.EventEmitter); 33 | 34 | 35 | /*--- Output Record Stream methods (Sendable) ---*/ 36 | 37 | /** 38 | * Output record into stream. 39 | * 40 | * @param {Record} record - Record object 41 | */ 42 | RecordStream.prototype.send = function(record) { 43 | // abstract 44 | }; 45 | 46 | /** 47 | * End sending records into stream. 48 | */ 49 | RecordStream.prototype.end = function() { 50 | this.sendable = false; 51 | }; 52 | 53 | /** 54 | * Destroy record stream; 55 | */ 56 | RecordStream.prototype.destroy = function() { 57 | this.reciebable = false; 58 | this.sendable = false; 59 | }; 60 | 61 | /** 62 | * Destroy record stream after all record submission in the queue; 63 | */ 64 | RecordStream.prototype.destroySoon = function() { 65 | // 66 | }; 67 | 68 | 69 | /*--- Input Record Stream methods (Receivable) ---*/ 70 | 71 | /* 72 | * Pause record fetch 73 | * @abstract 74 | */ 75 | RecordStream.prototype.pause = function() { 76 | // abstract 77 | }; 78 | 79 | /** 80 | * Resume record fetch and query execution 81 | * @abstract 82 | */ 83 | RecordStream.prototype.resume = function() { 84 | // abstract 85 | }; 86 | 87 | /** 88 | * Streaming pipe for record manipulation 89 | * Originally from Node.js's Stream#pipe 90 | * https://github.com/joyent/node/blob/master/lib/stream.js 91 | * 92 | * @param {RecordStream} dest - Destination output stream for records 93 | * @param {Object} [options] 94 | * @returns {RecordStream} 95 | */ 96 | RecordStream.prototype.pipe = function (dest, options) { 97 | var source = this; 98 | 99 | var onRecord = function(record) { 100 | if (dest.send && false === dest.send(record)) { source.pause(); } 101 | }; 102 | 103 | source.on('record', onRecord); 104 | 105 | var onDrain = function() { source.resume(); }; 106 | 107 | dest.on('drain', onDrain); 108 | 109 | var didOnEnd = false; 110 | var onEnd = function() { 111 | if (didOnEnd) { return; } 112 | didOnEnd = true; 113 | dest.end(); 114 | }; 115 | 116 | var onClose = function() { 117 | if (didOnEnd) { return; } 118 | didOnEnd = true; 119 | if (typeof dest.destroy === 'function') { dest.destroy(); } 120 | }; 121 | 122 | // If the 'end' option is not supplied, dest.end() will be called when 123 | // source gets the 'end' or 'close' events. Only dest.end() once. 124 | if (!options || options.end !== false) { 125 | source.on('end', onEnd); 126 | source.on('close', onClose); 127 | } 128 | 129 | // don't leave dangling pipes when there are errors. 130 | var onError = function(err) { 131 | cleanup(); 132 | if (this.listeners('error').length === 0) { 133 | throw err; // Unhandled stream error in pipe. 134 | } 135 | }; 136 | 137 | source.on('error', onError); 138 | dest.on('error', onError); 139 | 140 | // remove all the event listeners that were added. 141 | var cleanup = function() { 142 | source.removeListener('record', onRecord); 143 | dest.removeListener('drain', onDrain); 144 | 145 | source.removeListener('end', onEnd); 146 | source.removeListener('close', onClose); 147 | 148 | source.removeListener('error', onError); 149 | dest.removeListener('error', onError); 150 | 151 | source.removeListener('end', cleanup); 152 | source.removeListener('close', cleanup); 153 | 154 | dest.removeListener('end', cleanup); 155 | dest.removeListener('close', cleanup); 156 | }; 157 | 158 | source.on('end', cleanup); 159 | source.on('close', cleanup); 160 | 161 | dest.on('end', cleanup); 162 | dest.on('close', cleanup); 163 | 164 | dest.emit('pipe', source); 165 | 166 | // Allow for unix-like usage: A.pipe(B).pipe(C) 167 | return dest; 168 | }; 169 | 170 | 171 | /** 172 | * Mapping incoming record from upstream, and pass to downstream 173 | * 174 | * @param {RecordMapFunction} fn - Record mapping function 175 | * @returns {RecordStream} 176 | */ 177 | RecordStream.prototype.map = function(fn) { 178 | return this.pipe(RecordStream.map(fn)); 179 | }; 180 | 181 | /** 182 | * Filtering incoming record from upstream, and pass to downstream 183 | * 184 | * @param {RecordFilterFunction} fn - Record filtering function 185 | * @returns {RecordStream} 186 | */ 187 | RecordStream.prototype.filter = function(fn) { 188 | return this.pipe(RecordStream.filter(fn)); 189 | }; 190 | 191 | /** 192 | * Create Node.js stream instance for serializing/deserialize records 193 | * 194 | * @returns {stream.Stream} 195 | */ 196 | RecordStream.prototype.stream = function(type) { 197 | type = type || 'csv'; 198 | var recStream; 199 | if (type === "csv") { 200 | recStream = new RecordStream.CSVStream(); 201 | } 202 | if (!recStream) { 203 | throw new Error("No stream type defined for '"+type+"'."); 204 | } 205 | if (this.receivable) { 206 | this.pipe(recStream); 207 | } else if (this.sendable) { 208 | recStream.pipe(this); 209 | } 210 | return recStream.stream(); // get Node.js stream instance 211 | }; 212 | 213 | /* --------------------------------------------------- */ 214 | 215 | /** 216 | * @callback RecordMapFunction 217 | * @param {Record} record - Source record to map 218 | * @returns {Record} 219 | */ 220 | 221 | /** 222 | * Create a record stream which maps records and pass them to downstream 223 | * 224 | * @param {RecordMapFunction} fn - Record mapping function 225 | * @returns {RecordStream} 226 | */ 227 | RecordStream.map = function(fn) { 228 | var rstream = new RecordStream(); 229 | rstream.receivable = true; 230 | rstream.send = function(record) { 231 | var rec = fn(record) || record; // if not returned record, use same record 232 | this.emit('record', rec); 233 | }; 234 | return rstream; 235 | }; 236 | 237 | /** 238 | * Create mapping stream using given record template 239 | * 240 | * @param {Record} record - Mapping record object. In mapping field value, temlate notation can be used to refer field value in source record, if noeval param is not true. 241 | * @param {Boolean} [noeval] - Disable template evaluation in mapping record. 242 | * @returns {RecordStream} 243 | */ 244 | RecordStream.recordMapStream = function(record, noeval) { 245 | return RecordStream.map(function(rec) { 246 | var mapped = { Id: rec.Id }; 247 | for (var prop in record) { 248 | mapped[prop] = noeval ? record[prop] : evalMapping(record[prop], rec); 249 | } 250 | return mapped; 251 | }); 252 | 253 | function evalMapping(value, mapping) { 254 | if (_.isString(value)) { 255 | var m = /^\$\{(\w+)\}$/.exec(value); 256 | if (m) { return mapping[m[1]]; } 257 | return value.replace(/\$\{(\w+)\}/g, function($0, prop) { 258 | var v = mapping[prop]; 259 | return _.isNull(v) || _.isUndefined(v) ? "" : String(v); 260 | }); 261 | } else { 262 | return value; 263 | } 264 | } 265 | }; 266 | 267 | /** 268 | * @callback RecordFilterFunction 269 | * @param {Record} record - Source record to filter 270 | * @returns {Boolean} 271 | */ 272 | 273 | /** 274 | * Create a record stream which filters records and pass them to downstream 275 | * 276 | * @param {RecordFilterFunction} fn - Record filtering function 277 | * @returns {RecordStream} 278 | */ 279 | RecordStream.filter = function(fn) { 280 | var rstream = new RecordStream(); 281 | rstream.receivable = true; 282 | rstream.send = function(record) { 283 | if (fn(record)) { 284 | this.emit('record', record); 285 | } 286 | }; 287 | return rstream; 288 | }; 289 | 290 | 291 | /* --------------------------------------------------- */ 292 | 293 | /** 294 | * CSVStream (extends RecordStream implements Receivable, Sendable) 295 | * 296 | * @protected 297 | * @class RecordStream.CSVStream 298 | * @extends RecordStream 299 | */ 300 | var CSVStream = RecordStream.CSVStream = function(headers) { 301 | var self = this; 302 | this.sendable = true; 303 | this.receivable = true; 304 | this.headers = headers; 305 | this.wroteHeaders = false; 306 | this._stream = new Stream(); 307 | this._buffer = []; 308 | this._stream.on('data', function(data) { self._handleData(data); }); 309 | this._stream.on('end', function(data) { self._handleEnd(data); }); 310 | }; 311 | 312 | util.inherits(CSVStream, RecordStream); 313 | 314 | /** 315 | * 316 | * @override 317 | * @method RecordStream.CSVStream#send 318 | * @param {Record} record - Record object 319 | */ 320 | CSVStream.prototype.send = function(record) { 321 | if (!this.wroteHeaders) { 322 | if (!this.headers) { 323 | this.headers = CSV.extractHeaders([ record ]); 324 | } 325 | this._stream.emit("data", CSV.arrayToCSV(this.headers) + "\n"); 326 | this.wroteHeaders = true; 327 | } 328 | this._stream.emit("data", CSV.recordToCSV(record, this.headers) + "\n"); 329 | }; 330 | 331 | /** 332 | * 333 | * @override 334 | * @method RecordStream.CSVStream#end 335 | * @param {Record} record - Record object 336 | */ 337 | CSVStream.prototype.end = function(record) { 338 | if (record) { this.send(record); } 339 | this.readable = false; 340 | this.sendable = false; 341 | this._stream.emit("end"); 342 | }; 343 | 344 | /** 345 | * @private 346 | */ 347 | CSVStream.prototype._handleData = function(data, enc) { 348 | this._buffer.push([ data, enc ]); 349 | }; 350 | 351 | /** 352 | * @private 353 | */ 354 | CSVStream.prototype._handleEnd = function(data, enc) { 355 | var self = this; 356 | if (data) { 357 | this._buffer.push([ data, enc ]); 358 | } 359 | data = this._buffer.map(function(d) { 360 | return d[0].toString(d[1] || 'utf-8'); 361 | }).join(''); 362 | var records = CSV.parseCSV(data); 363 | records.forEach(function(record) { 364 | self.emit('record', record); 365 | }); 366 | this.emit('end'); 367 | }; 368 | 369 | /** 370 | * Get delegating Node.js stream 371 | * @override 372 | * @method RecordStream.CSVStream#stream 373 | */ 374 | CSVStream.prototype.stream = function(record) { 375 | return this._stream; 376 | }; 377 | 378 | -------------------------------------------------------------------------------- /lib/record.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Represents Salesforce record information 3 | * @author Shinichi Tomita 4 | */ 5 | var _ = require('underscore')._; 6 | 7 | /** 8 | * A simple hash object including record field information 9 | * 10 | * @typedef {Object} Record 11 | */ 12 | 13 | /** 14 | * Remote reference to record information 15 | * 16 | * @protected 17 | * @class 18 | * @constructor 19 | * @param {Connection} conn - Connection object 20 | * @param {String} type - SObject type 21 | * @param {String} id - Record ID 22 | */ 23 | var RecordReference = module.exports = function(conn, type, id) { 24 | this._conn = conn; 25 | this.type = type; 26 | this.id = id; 27 | }; 28 | 29 | /** 30 | * Retrieve record field information 31 | * 32 | * @param {Callback.} [callback] - Callback function 33 | * @returns {Promise.} 34 | */ 35 | RecordReference.prototype.retrieve = function(callback) { 36 | return this._conn.retrieve(this.type, this.id, callback); 37 | }; 38 | 39 | /** 40 | * Update record field information 41 | * 42 | * @param {Record} record - A Record which includes fields to update 43 | * @param {Callback.} [callback] - Callback function 44 | * @returns {Promise.} 45 | */ 46 | RecordReference.prototype.update = function(record, callback) { 47 | record = _.clone(record); 48 | record.Id = this.id; 49 | return this._conn.update(this.type, record, callback); 50 | }; 51 | 52 | /** 53 | * Synonym of Record#destroy() 54 | * 55 | * @method RecordReference#delete 56 | * @param {Callback.} [callback] - Callback function 57 | * @returns {Promise.} 58 | */ 59 | RecordReference.prototype["delete"] = 60 | /** 61 | * Synonym of Record#destroy() 62 | * 63 | * @method RecordReference#del 64 | * @param {Callback.} [callback] - Callback function 65 | * @returns {Promise.} 66 | */ 67 | RecordReference.prototype.del = 68 | /** 69 | * Delete record field 70 | * 71 | * @method RecordReference#destroy 72 | * @param {Callback.} [callback] - Callback function 73 | * @returns {Promise.} 74 | */ 75 | RecordReference.prototype.destroy = function(callback) { 76 | return this._conn.destroy(this.type, this.id, callback); 77 | }; 78 | 79 | /** 80 | * Get blob field as stream 81 | * 82 | * @param {String} fieldName - Blob field name 83 | * @returns {stream.Stream} 84 | */ 85 | RecordReference.prototype.blob = function(fieldName) { 86 | var url = [ this._conn._baseUrl(), 'sobjects', this.type, this.id, fieldName ].join('/'); 87 | return this._conn._request(url).stream(); 88 | }; 89 | 90 | -------------------------------------------------------------------------------- /lib/repl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Creates REPL interface with built in Salesforce API objects and automatically resolves promise object 3 | * @author Shinichi Tomita 4 | */ 5 | var _ = require('underscore')._, 6 | sf = require('./salesforce'); 7 | 8 | /** 9 | * Intercept the evaled value when it is "promise", and resolve its value before sending back to REPL. 10 | * @private 11 | */ 12 | function promisify(repl) { 13 | var _eval = repl.eval; 14 | repl.eval = function(cmd, context, filename, callback) { 15 | _eval.call(repl, cmd, context, filename, function(err, res) { 16 | if (isPromiseLike(res)) { 17 | res.then(function(ret) { 18 | callback(null, ret); 19 | }, function(err) { 20 | callback(err); 21 | }); 22 | } else { 23 | callback(err, res); 24 | } 25 | }); 26 | }; 27 | return repl; 28 | } 29 | 30 | /** 31 | * Detect whether the value has CommonJS Promise/A+ interface or not 32 | * @private 33 | */ 34 | function isPromiseLike(v) { 35 | return _.isObject(v) && _.isFunction(v.then); 36 | } 37 | 38 | /** 39 | * define get accessor using Object.defineProperty 40 | * @private 41 | */ 42 | function defineProp(target, obj, prop) { 43 | if (Object.defineProperty) { 44 | Object.defineProperty(target, prop, { 45 | get: function() { return obj[prop]; } 46 | }); 47 | } 48 | } 49 | 50 | /** 51 | * Map all node-salesforce object to REPL context 52 | * @private 53 | */ 54 | function defineBuiltinVars(context) { 55 | // define salesforce package root objects 56 | for (var key in sf) { 57 | if (sf.hasOwnProperty(key) && !global[key]) { 58 | context[key] = sf[key]; 59 | } 60 | } 61 | // expose salesforce package root as "$sf" in context. 62 | context.$sf = sf; 63 | 64 | // create default connection object 65 | var conn = new sf.Connection(); 66 | 67 | for (var prop in conn) { 68 | if (prop.indexOf('_') === 0) { // ignore private 69 | continue; 70 | } 71 | if (_.isFunction(conn[prop])) { 72 | context[prop] = _.bind(conn[prop], conn); 73 | } else if (_.isObject(conn[prop])) { 74 | defineProp(context, conn, prop); 75 | } 76 | } 77 | 78 | // expose default connection as "$conn" 79 | context.$conn = conn; 80 | } 81 | 82 | /** 83 | * @protected 84 | */ 85 | module.exports = function(repl) { 86 | return { 87 | start: function(options) { 88 | var sfrepl = promisify(repl.start(options)); 89 | defineBuiltinVars(sfrepl.context); 90 | return sfrepl; 91 | } 92 | }; 93 | }; 94 | 95 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream'), 2 | _ = require('underscore')._, 3 | request = require('request'), 4 | Promise = require('./promise'); 5 | 6 | /** 7 | * HTTP request method, returns promise instead of stream 8 | * @private 9 | */ 10 | function promisedRequest(params, callback) { 11 | var deferred = Promise.defer(); 12 | var req; 13 | var createRequest = function() { 14 | if (!req) { 15 | req = request(params, function(err, response) { 16 | if (err) { 17 | deferred.reject(err); 18 | } else { 19 | deferred.resolve(response); 20 | } 21 | }); 22 | } 23 | return req; 24 | }; 25 | return streamify(deferred.promise, createRequest).thenCall(callback); 26 | } 27 | 28 | /** 29 | * Add stream() method to promise (and following promise chain), to access original request stream. 30 | * @private 31 | */ 32 | function streamify(promise, factory) { 33 | var _then = promise.then; 34 | promise.then = function() { 35 | factory(); 36 | var newPromise = _then.apply(promise, arguments); 37 | return streamify(newPromise, factory); 38 | }; 39 | promise.stream = factory; 40 | return promise; 41 | } 42 | 43 | /** 44 | * @protected 45 | */ 46 | module.exports = promisedRequest; 47 | -------------------------------------------------------------------------------- /lib/salesforce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Node-salesforce API root object 3 | * @author Shinichi Tomita 4 | */ 5 | exports.Connection = require('./connection'); 6 | exports.OAuth2 = require('./oauth2'); 7 | exports.Date = exports.SfDate = require("./date"); 8 | exports.RecordStream = require('./record-stream'); 9 | -------------------------------------------------------------------------------- /lib/soap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages method call to SOAP endpoint 3 | * @author Shinichi Tomita 4 | */ 5 | var _ = require('underscore'), 6 | xml2js = require('xml2js'), 7 | request = require('./request'); 8 | 9 | /** 10 | * Class for SOAP endpoint of Salesforce 11 | * 12 | * @protected 13 | * @class 14 | * @constructor 15 | * @param {Object} options - SOAP endpoint setting options 16 | * @param {String} options.serverUrl - SOAP endpoint URL 17 | * @param {String} options.sessionId - Salesforce session ID 18 | * @param {String} [options.xmlns] - XML namespace for method call (default is "urn:partner.soap.sforce.com") 19 | */ 20 | var SOAP = module.exports = function(options) { 21 | this.serverUrl = options.serverUrl; 22 | this.sessionId = options.sessionId; 23 | this.xmlns = options.xmlns || 'urn:partner.soap.sforce.com'; 24 | }; 25 | 26 | /** 27 | * Invoke SOAP call using method and arguments 28 | * 29 | * @param {String} method - Method name 30 | * @param {Object} args - Arguments for the method call 31 | * @param {Callback.} [callback] - Callback function 32 | * @returns {Promise.} 33 | */ 34 | SOAP.prototype.invoke = function(method, args, callback) { 35 | var message = {}; 36 | message[method] = args; 37 | var soapEnvelope = this._createEnvelope(message); 38 | return request({ 39 | method: 'POST', 40 | url: this.serverUrl, 41 | headers: { 42 | 'Content-Type': 'text/xml', 43 | 'SOAPAction': '""' 44 | }, 45 | body: soapEnvelope 46 | }).then(function(res) { 47 | var ret = null; 48 | xml2js.parseString(res.body, { explicitArray: false }, function(err, value) { ret = value; }); 49 | if (ret) { 50 | var error = lookupValue(ret, [ /:Envelope$/, /:Body$/, /:Fault$/, /faultstring$/ ]); 51 | if (error) { 52 | throw new Error(error); 53 | } 54 | return lookupValue(ret, [ /:Envelope$/, /:Body$/, /.+/ ]); 55 | } 56 | throw new Error("invalid response"); 57 | }).thenCall(callback); 58 | }; 59 | 60 | /** 61 | * @private 62 | */ 63 | function lookupValue(obj, propRegExps) { 64 | var regexp = propRegExps.shift(); 65 | if (!regexp) { 66 | return obj; 67 | } 68 | else { 69 | for (var prop in obj) { 70 | if (regexp.test(prop)) { 71 | return lookupValue(obj[prop], propRegExps); 72 | } 73 | } 74 | return null; 75 | } 76 | } 77 | 78 | /** 79 | * @private 80 | */ 81 | function toXML(name, value) { 82 | if (_.isObject(name)) { 83 | value = name; 84 | name = null; 85 | } 86 | if (_.isArray(value)) { 87 | return _.map(value, function(v) { return toXML(name, v); }).join(''); 88 | } else { 89 | var attrs = []; 90 | var elems = []; 91 | if (_.isObject(value)) { 92 | for (var k in value) { 93 | var v = value[k]; 94 | if (k[0] === '@') { 95 | k = k.substring(1); 96 | attrs.push(k + '="' + v + '"'); 97 | } else { 98 | elems.push(toXML(k, v)); 99 | } 100 | } 101 | value = elems.join(''); 102 | } else { 103 | value = String(value); 104 | } 105 | var startTag = name ? '<' + name + (attrs.length > 0 ? ' ' + attrs.join(' ') : '') + '>' : ''; 106 | var endTag = name ? '' : ''; 107 | return startTag + value + endTag; 108 | } 109 | } 110 | 111 | /** 112 | * @private 113 | */ 114 | SOAP.prototype._createEnvelope = function(message) { 115 | return [ 116 | '', 117 | '', 120 | '', 121 | '', 122 | '' + this.sessionId + '', 123 | '', 124 | '', 125 | '', 126 | toXML(message), 127 | '', 128 | '' 129 | ].join(''); 130 | }; 131 | -------------------------------------------------------------------------------- /lib/sobject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Represents Salesforce SObject 3 | * @author Shinichi Tomita 4 | */ 5 | var _ = require('underscore'), 6 | Record = require('./record'), 7 | Query = require('./query'), 8 | Cache = require('./cache'); 9 | 10 | /** 11 | * A class for organizing all SObject access 12 | * 13 | * @constructor 14 | */ 15 | var SObject = module.exports = function(conn, type) { 16 | this._conn = conn; 17 | this.type = type; 18 | var cacheOptions = { key: "describe." + this.type }; 19 | this.describe$ = conn.cache.makeCacheable(this.describe, this, cacheOptions); 20 | this.describe = conn.cache.makeResponseCacheable(this.describe, this, cacheOptions); 21 | }; 22 | 23 | /** 24 | * Synonym of SObject#create() 25 | * 26 | * @method SObject#insert 27 | * @param {Record|Array.} records - A record or array of records to create 28 | * @param {Callback.>} [callback] - Callback function 29 | * @returns {Promise.>} 30 | */ 31 | /** 32 | * Create records 33 | * 34 | * @method SObject#create 35 | * @param {Record|Array.} records - A record or array of records to create 36 | * @param {Callback.>} [callback] - Callback function 37 | * @returns {Promise.>} 38 | */ 39 | SObject.prototype.insert = 40 | SObject.prototype.create = function(records, callback) { 41 | return this._conn.create(this.type, records, callback); 42 | }; 43 | 44 | /** 45 | * Retrieve specified records 46 | * 47 | * @param {String|Array.} ids - A record ID or array of record IDs 48 | * @param {Callback.>} [callback] - Callback function 49 | * @returns {Promise.>} 50 | */ 51 | SObject.prototype.retrieve = function(ids, callback) { 52 | return this._conn.retrieve(this.type, ids, callback); 53 | }; 54 | 55 | /** 56 | * Update records 57 | * 58 | * @param {Record|Array.} records - A record or array of records to update 59 | * @param {Callback.>} [callback] - Callback function 60 | * @returns {Promise.>} 61 | */ 62 | SObject.prototype.update = function(records, callback) { 63 | return this._conn.update(this.type, records, callback); 64 | }; 65 | 66 | /** 67 | * Upsert records 68 | * 69 | * @param {Record|Array.} records - Record or array of records to upsert 70 | * @param {String} extIdField - External ID field name 71 | * @param {Callback.>} [callback] - Callback 72 | * @returns {Promise.>} 73 | */ 74 | SObject.prototype.upsert = function(records, extIdField, callback) { 75 | return this._conn.upsert(this.type, records, extIdField, callback); 76 | }; 77 | 78 | /** 79 | * Synonym of SObject#destroy() 80 | * 81 | * @method SObject#delete 82 | * @param {String|Array.} ids - A ID or array of IDs to delete 83 | * @param {Callback.>} [callback] - Callback function 84 | * @returns {Promise.>} 85 | */ 86 | /** 87 | * Synonym of SObject#destroy() 88 | * 89 | * @method SObject#del 90 | * @param {String|Array.} ids - A ID or array of IDs to delete 91 | * @param {Callback.>} [callback] - Callback function 92 | * @returns {Promise.>} 93 | */ 94 | /** 95 | * Delete records 96 | * 97 | * @method SObject#destroy 98 | * @param {String|Array.} ids - A ID or array of IDs to delete 99 | * @param {Callback.>} [callback] - Callback function 100 | * @returns {Promise.>} 101 | */ 102 | SObject.prototype["delete"] = 103 | SObject.prototype.del = 104 | SObject.prototype.destroy = function(ids, callback) { 105 | return this._conn.destroy(this.type, ids, callback); 106 | }; 107 | 108 | /** 109 | * Describe SObject metadata 110 | * 111 | * @param {Callback.} [callback] - Callback function 112 | * @returns {Promise.} 113 | */ 114 | SObject.prototype.describe = function(callback) { 115 | return this._conn.describe(this.type, callback); 116 | }; 117 | 118 | /** 119 | * Get record representation instance by given id 120 | * 121 | * @param {String} id - A record ID 122 | * @returns {RecordReference} 123 | */ 124 | SObject.prototype.record = function(id) { 125 | return new Record(this._conn, this.type, id); 126 | }; 127 | 128 | /** 129 | * Find and fetch records which matches given conditions 130 | * 131 | * @param {Object|String} [conditions] - Conditions in JSON object (MongoDB-like), or raw SOQL WHERE clause string. 132 | * @param {Object|Array.|String} [fields] - Fields to fetch. Format can be in JSON object (MongoDB-like), array of field names, or comma-separated field names. 133 | * @param {Object} [options] - Query options. 134 | * @param {Number} [options.limit] - Maximum number of records the query will return. 135 | * @param {Number} [options.offset] - Offset number where begins returning results. 136 | * @param {Number} [options.skip] - Synonym of options.offset. 137 | * @param {Callback.>} [callback] - Callback function 138 | * @returns {Query.>} 139 | */ 140 | SObject.prototype.find = function(conditions, fields, options, callback) { 141 | if (typeof conditions === 'function') { 142 | callback = conditions; 143 | conditions = {}; 144 | fields = null; 145 | options = null; 146 | } else if (typeof fields === 'function') { 147 | callback = fields; 148 | fields = null; 149 | options = null; 150 | } else if (typeof options === 'function') { 151 | callback = options; 152 | options = null; 153 | } 154 | options = options || {}; 155 | var config = { 156 | fields: fields, 157 | includes: options.includes, 158 | table: this.type, 159 | conditions: conditions, 160 | limit: options.limit, 161 | offset: options.offset || options.skip 162 | }; 163 | var query = new Query(this._conn, config); 164 | query.setResponseTarget(Query.ResponseTargets.Records); 165 | if (callback) { query.run(callback); } 166 | return query; 167 | }; 168 | 169 | /** 170 | * Fetch one record which matches given conditions 171 | * 172 | * @param {Object|String} [conditions] - Conditions in JSON object (MongoDB-like), or raw SOQL WHERE clause string. 173 | * @param {Object|Array.|String} [fields] - Fields to fetch. Format can be in JSON object (MongoDB-like), array of field names, or comma-separated field names. 174 | * @param {Object} [options] - Query options. 175 | * @param {Number} [options.limit] - Maximum number of records the query will return. 176 | * @param {Number} [options.offset] - Offset number where begins returning results. 177 | * @param {Number} [options.skip] - Synonym of options.offset. 178 | * @param {Callback.} [callback] - Callback function 179 | * @returns {Query.} 180 | */ 181 | SObject.prototype.findOne = function(conditions, fields, options, callback) { 182 | if (typeof conditions === 'function') { 183 | callback = conditions; 184 | conditions = {}; 185 | fields = null; 186 | options = null; 187 | } else if (typeof fields === 'function') { 188 | callback = fields; 189 | fields = null; 190 | options = null; 191 | } else if (typeof options === 'function') { 192 | callback = options; 193 | options = null; 194 | } 195 | options = _.extend(options || {}, { limit: 1 }); 196 | var query = this.find(conditions, fields, options); 197 | query.setResponseTarget(Query.ResponseTargets.SingleRecord); 198 | if (callback) { query.run(callback); } 199 | return query; 200 | }; 201 | 202 | /** 203 | * Find and fetch records only by specifying fields to fetch. 204 | * 205 | * @param {Object|Array.|String} [fields] - Fields to fetch. Format can be in JSON object (MongoDB-like), array of field names, or comma-separated field names. 206 | * @param {Callback.>} [callback] - Callback function 207 | * @returns {Query.>} 208 | */ 209 | SObject.prototype.select = function(fields, callback) { 210 | return this.find(null, fields, null, callback); 211 | }; 212 | 213 | /** 214 | * Count num of records which matches given conditions 215 | * 216 | * @param {Object|String} [conditions] - Conditions in JSON object (MongoDB-like), or raw SOQL WHERE clause string. 217 | * @param {Callback.} [callback] - Callback function 218 | * @returns {Query.} 219 | */ 220 | SObject.prototype.count = function(conditions, callback) { 221 | if (typeof conditions === 'function') { 222 | callback = conditions; 223 | conditions = {}; 224 | } 225 | var query = this.find(conditions, { "count()" : true }); 226 | query.setResponseTarget("Count"); 227 | if (callback) { query.run(callback); } 228 | return query; 229 | }; 230 | 231 | 232 | /** 233 | * Call Bulk#load() to execute bulkload, returning batch object 234 | * 235 | * @param {String} operation - Bulk load operation ('insert', 'update', 'upsert', 'delete', or 'hardDelete') 236 | * @param {Object} [options] - Options for bulk loading operation 237 | * @param {String} [options.extIdField] - External ID field name (used when upsert operation). 238 | * @param {Array.|stream.Stream|String} [input] - Input source for bulkload. Accepts array of records, CSv string, and CSV data input stream. 239 | * @param {Callback.>} [callback] - Callback function 240 | * @returns {Bulk~Batch} 241 | */ 242 | SObject.prototype.bulkload = function(operation, options, input, callback) { 243 | return this._conn.bulk.load(this.type, operation, options, input, callback); 244 | }; 245 | 246 | /** 247 | * Synonym of SObject#createBulk() 248 | * 249 | * @method SObject#insertBulk 250 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk insert. Accepts array of records, CSv string, and CSV data input stream. 251 | * @param {Callback.>} [callback] - Callback function 252 | * @returns {Bulk~Batch} 253 | */ 254 | /** 255 | * Bulkly insert input data using bulk API 256 | * 257 | * @method SObject#createBulk 258 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk insert. Accepts array of records, CSv string, and CSV data input stream. 259 | * @param {Callback.>} [callback] - Callback function 260 | * @returns {Bulk~Batch} 261 | */ 262 | SObject.prototype.insertBulk = 263 | SObject.prototype.createBulk = function(input, callback) { 264 | return this.bulkload("insert", input, callback); 265 | }; 266 | 267 | /** 268 | * Bulkly update records by input data using bulk API 269 | * 270 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk update Accepts array of records, CSv string, and CSV data input stream. 271 | * @param {Callback.>} [callback] - Callback function 272 | * @returns {Bulk~Batch} 273 | */ 274 | SObject.prototype.updateBulk = function(input, callback) { 275 | return this.bulkload("update", input, callback); 276 | }; 277 | 278 | /** 279 | * Bulkly upsert records by input data using bulk API 280 | * 281 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk upsert. Accepts array of records, CSv string, and CSV data input stream. 282 | * @param {String} [options.extIdField] - External ID field name 283 | * @param {Callback.>} [callback] - Callback function 284 | * @returns {Bulk~Batch} 285 | */ 286 | SObject.prototype.upsertBulk = function(input, extIdField, callback) { 287 | return this.bulkload("upsert", { extIdField: extIdField }, input, callback); 288 | }; 289 | 290 | /** 291 | * Synonym of SObject#destroyBulk() 292 | * 293 | * @method SObject#deleteBulk 294 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk delete. Accepts array of records, CSv string, and CSV data input stream. 295 | * @param {Callback.>} [callback] - Callback function 296 | * @returns {Bulk~Batch} 297 | */ 298 | /** 299 | * Bulkly delete records specified by input data using bulk API 300 | * 301 | * @method SObject#destroyBulk 302 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk delete. Accepts array of records, CSv string, and CSV data input stream. 303 | * @param {Callback.>} [callback] - Callback function 304 | * @returns {Bulk~Batch} 305 | */ 306 | SObject.prototype.deleteBulk = 307 | SObject.prototype.destroyBulk = function(input, callback) { 308 | return this.bulkload("delete", input, callback); 309 | }; 310 | 311 | /** 312 | * Synonym of SObject#destroyHardBulk() 313 | * 314 | * @method SObject#deleteHardBulk 315 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk delete. Accepts array of records, CSv string, and CSV data input stream. 316 | * @param {Callback.>} [callback] - Callback function 317 | * @returns {Bulk~Batch} 318 | */ 319 | /** 320 | * Bulkly hard delete records specified in input data using bulk API 321 | * 322 | * @method SObject#destroyHardBulk 323 | * @param {Array.|stream.Stream|String} [input] - Input source for bulk delete. Accepts array of records, CSv string, and CSV data input stream. 324 | * @param {Callback.>} [callback] - Callback function 325 | * @returns {Bulk~Batch} 326 | */ 327 | SObject.prototype.deleteHardBulk = 328 | SObject.prototype.destroyHardBulk = function(input, callback) { 329 | return this.bulkload("hardDelete", input, callback); 330 | }; 331 | 332 | /** 333 | * Retrieve the updated records 334 | * 335 | * @param {String|Date} start - start date or string representing the start of the interval 336 | * @param {String|Date} end - start date or string representing the end of the interval, must be > start 337 | * @param {Callback.} [callback] - Callback function 338 | * @returns {Promise.} 339 | */ 340 | SObject.prototype.updated = function (start, end, callback) { 341 | return this._conn.updated(this.type, start, end, callback); 342 | }; 343 | 344 | /** 345 | * Retrieve the deleted records 346 | * 347 | * @param {String|Date} start - start date or string representing the start of the interval 348 | * @param {String|Date} end - start date or string representing the end of the interval, must be > start 349 | * @param {Callback.} [callback] - Callback function 350 | * @returns {Promise.} 351 | */ 352 | SObject.prototype.deleted = function (start, end, callback) { 353 | return this._conn.deleted(this.type, start, end, callback); 354 | }; -------------------------------------------------------------------------------- /lib/soql-builder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Create and build SOQL string from configuration 3 | * @author Shinichi Tomita 4 | */ 5 | var _ = require("underscore"), 6 | SfDate = require("./date"); 7 | 8 | 9 | /** 10 | * Create SOQL 11 | * @private 12 | */ 13 | function createSOQL(query) { 14 | var soql = [ 15 | "SELECT ", 16 | createFieldsClause(query.fields, query.includes), 17 | " FROM ", 18 | query.table 19 | ].join(""); 20 | var cond = createConditionClause(query.conditions); 21 | if (cond) { 22 | soql += " WHERE " + cond; 23 | } 24 | var orderby = createOrderByClause(query.sort); 25 | if (orderby) { 26 | soql += " ORDER BY " + orderby; 27 | } 28 | if (query.limit) { 29 | soql += " LIMIT " + query.limit; 30 | } 31 | if (query.offset) { 32 | soql += " OFFSET " + query.offset; 33 | } 34 | return soql; 35 | } 36 | 37 | /** @private **/ 38 | function createFieldsClause(fields, childQueries) { 39 | childQueries = _.map(_.values(childQueries || {}), function(cquery) { 40 | return '(' + createSOQL(cquery) + ')'; 41 | }); 42 | return (fields || [ "Id" ]).concat(childQueries).join(', '); 43 | } 44 | 45 | /** @private **/ 46 | function createConditionClause(conditions, operator, depth) { 47 | if (_.isString(conditions)) { 48 | return conditions; 49 | } 50 | conditions = conditions || []; 51 | operator = operator || "AND"; 52 | depth = depth || 0; 53 | if (!isArray(conditions)) { // if passed in hash object 54 | conditions = _.keys(conditions).map(function(key) { 55 | return { 56 | key: key, 57 | value: conditions[key] 58 | }; 59 | }); 60 | } else { 61 | conditions = conditions.map(function(cond) { 62 | var conds = []; 63 | for (var key in cond) { 64 | conds.push({ 65 | key: key, 66 | value: cond[key] 67 | }); 68 | } 69 | return conds.length>1 ? conds : conds[0]; 70 | }); 71 | } 72 | conditions = conditions.map(function(cond) { 73 | var d = depth+1, op; 74 | switch (cond.key) { 75 | case "$or" : 76 | case "$and" : 77 | case "$not" : 78 | if (operator !== "NOT" && conditions.length === 1) { 79 | d = depth; // not change tree depth 80 | } 81 | op = cond.key === "$or" ? "OR" : 82 | cond.key === "$and" ? "AND" : 83 | "NOT"; 84 | return createConditionClause(cond.value, op, d); 85 | default: 86 | return createFieldExpression(cond.key, cond.value); 87 | } 88 | }).filter(function(expr) { return expr; }); 89 | 90 | var paren; 91 | if (operator === 'NOT') { 92 | paren = depth > 0; 93 | return (paren ? "(" : "") + "NOT " + conditions[0] + (paren ? ")" : ""); 94 | } else { 95 | paren = depth > 0 && conditions.length > 1; 96 | return (paren ? "(" : "") + conditions.join(" "+operator+" ") + (paren ? ")" : ""); 97 | } 98 | } 99 | 100 | var opMap = { 101 | "=" : "=", 102 | "$eq" : "=", 103 | "!=" : "!=", 104 | "$ne" : "!=", 105 | ">" : ">", 106 | "$gt" : ">", 107 | "<" : "<", 108 | "$lt" : "<", 109 | ">=" : ">=", 110 | "$gte" : ">=", 111 | "<=" : "<=", 112 | "$lte" : "<=", 113 | "$like" : "LIKE", 114 | "$nlike" : "NOT LIKE", 115 | "$in" : "IN", 116 | "$nin" : "NOT IN", 117 | "$exists" : "EXISTS" 118 | }; 119 | 120 | /** @private **/ 121 | function createFieldExpression(field, value) { 122 | var op = "$eq"; 123 | if (_.isObject(value)) { 124 | var _value; 125 | for (var k in value) { 126 | if (k[0] === "$") { 127 | op = k; 128 | value = value[k]; 129 | break; 130 | } 131 | } 132 | } 133 | var sfop = opMap[op]; 134 | if (!sfop || _.isUndefined(value)) { return null; } 135 | var valueExpr = createValueExpression(value); 136 | if (_.isUndefined(valueExpr)) { return null; } 137 | switch (sfop) { 138 | case "NOT LIKE": 139 | return "(" + [ "NOT", field, 'LIKE', valueExpr ].join(" ") + ")"; 140 | case "EXISTS": 141 | return [ field, value ? "!=" : "=", "null" ].join(" "); 142 | default: 143 | return [ field, sfop, valueExpr ].join(" "); 144 | } 145 | } 146 | 147 | /** @private **/ 148 | function createValueExpression(value) { 149 | if (isArray(value)) { 150 | return value.length > 0 ? 151 | "(" + value.map(createValueExpression).join(", ") + ")" : 152 | undefined; 153 | } 154 | if (value instanceof SfDate) { 155 | return value.toString(); 156 | } 157 | if (_.isString(value)) { 158 | return "'" + escapeSOQLString(value) + "'"; 159 | } 160 | if (_.isNumber(value)) { 161 | return (value).toString(); 162 | } 163 | if (_.isNull(value)) { 164 | return "null"; 165 | } 166 | return value; 167 | } 168 | 169 | /** @private **/ 170 | function escapeSOQLString(str) { 171 | return String(str || '').replace(/'/g, "\\'"); 172 | } 173 | 174 | /** @private **/ 175 | function isArray(a) { 176 | return _.isObject(a) && _.isFunction(a.pop); 177 | } 178 | 179 | 180 | /** @private **/ 181 | function createOrderByClause(sort) { 182 | sort = sort || []; 183 | if (_.isString(sort)) { 184 | if (/,|\s+(asc|desc)\s*$/.test(sort)) { 185 | // must be specified in pure "order by" clause. Return raw config. 186 | return sort; 187 | } 188 | // sort order in mongoose-style expression. 189 | // e.g. "FieldA -FieldB" => "ORDER BY FieldA ASC, FieldB DESC" 190 | sort = sort.split(/\s+/).map(function(field) { 191 | var dir = "ASC"; // ascending 192 | var flag = field[0]; 193 | if (flag === '-') { 194 | dir = "DESC"; 195 | field = field.substring(1); 196 | } else if (flag === '+') { 197 | field = field.substring(1); 198 | } 199 | return [ field, dir ]; 200 | }); 201 | } else if (!isArray(sort)) { 202 | sort = _.keys(sort).map(function(field) { 203 | var dir = sort[field]; 204 | return [ field, dir ]; 205 | }); 206 | } 207 | return sort.map(function(s) { 208 | var field = s[0], dir = s[1]; 209 | switch (String(dir)) { 210 | case "DESC": 211 | case "desc": 212 | case "descending": 213 | case "-": 214 | case "-1": 215 | dir = "DESC"; 216 | break; 217 | default: 218 | dir = "ASC"; 219 | } 220 | return field + " " + dir; 221 | }).join(", "); 222 | } 223 | 224 | 225 | exports.createSOQL = createSOQL; 226 | 227 | -------------------------------------------------------------------------------- /lib/streaming.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages Streaming APIs 3 | * @author Shinichi Tomita 4 | */ 5 | 6 | var events = require('events'), 7 | util = require('util'), 8 | request = require('request'), 9 | async = require('async'), 10 | _ = require('underscore')._, 11 | Faye = require('faye'); 12 | 13 | /** 14 | * Streaming API topic class 15 | * 16 | * @class Streaming~Topic 17 | * @param {Streaming} steaming - Streaming API object 18 | * @param {String} name - Topic name 19 | */ 20 | var Topic = module.exports = function(streaming, name) { 21 | this._streaming = streaming; 22 | this.name = name; 23 | }; 24 | 25 | /** 26 | * @typedef {Object} Streaming~StreamingMessage 27 | * @prop {Object} event 28 | * @prop {Object} event.type - Event type 29 | * @prop {Record} sobject - Record information 30 | */ 31 | /** 32 | * Subscribe listener to topic 33 | * 34 | * @method Streaming~Topic#subscribe 35 | * @param {Callback.} listener - Streaming message listener 36 | * @returns {Streaming~Topic} 37 | */ 38 | Topic.prototype.subscribe = function(listener) { 39 | this._streaming.subscribe(this.name, listener); 40 | return this; 41 | }; 42 | 43 | /** 44 | * Unsubscribe listener from topic 45 | * 46 | * @method Streaming~Topic#unsubscribe 47 | * @param {Callback.} listener - Streaming message listener 48 | * @returns {Streaming~Topic} 49 | */ 50 | Topic.prototype.unsubscribe = function(listener) { 51 | this._streaming.unsubscribe(this.name, listener); 52 | return this; 53 | }; 54 | 55 | /*--------------------------------------------*/ 56 | 57 | /** 58 | * Streaming API class 59 | * 60 | * @class 61 | * @extends events.EventEmitter 62 | * @param {Connection} conn - Connection object 63 | */ 64 | var Streaming = function(conn) { 65 | this._conn = conn; 66 | }; 67 | 68 | util.inherits(Streaming, events.EventEmitter); 69 | 70 | /** @private **/ 71 | Streaming.prototype._baseUrl = function(name) { 72 | return [ this._conn.instanceUrl, "cometd", this._conn.version ].join('/'); 73 | }; 74 | 75 | /** 76 | * Get named topic 77 | * 78 | * @param {String} name - Topic name 79 | * @returns {Streaming~Topic} 80 | */ 81 | Streaming.prototype.topic = function(name) { 82 | this._topics = this._topics || {}; 83 | var topic = this._topics[name] = 84 | this._topics[name] || new Topic(this, name); 85 | return topic; 86 | }; 87 | 88 | /** 89 | * Subscribe topic 90 | * 91 | * @param {String} name - Topic name 92 | * @param {Callback.} listener - Streaming message listener 93 | * @returns {Streaming} 94 | */ 95 | Streaming.prototype.subscribe = function(name, listener) { 96 | if (!this._fayeClient) { 97 | Faye.Transport.NodeHttp.prototype.batching = false; // prevent streaming API server error 98 | this._fayeClient = new Faye.Client(this._baseUrl(), {}); 99 | this._fayeClient.setHeader('Authorization', 'OAuth '+this._conn.accessToken); 100 | } 101 | this._fayeClient.subscribe("/topic/"+name, listener); 102 | return this; 103 | }; 104 | 105 | /** 106 | * Unsubscribe topic 107 | * 108 | * @param {String} name - Topic name 109 | * @param {Callback.} listener - Streaming message listener 110 | * @returns {Streaming} 111 | */ 112 | Streaming.prototype.unsubscribe = function(name, listener) { 113 | if (this._fayeClient) { 114 | this._fayeClient.unsubscribe("/topic/"+name, listener); 115 | } 116 | return this; 117 | }; 118 | 119 | module.exports = Streaming; 120 | -------------------------------------------------------------------------------- /lib/tooling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Manages Tooling APIs 3 | * @author Shinichi Tomita 4 | */ 5 | 6 | var util = require('util'), 7 | _ = require('underscore')._, 8 | Cache = require('./cache'); 9 | 10 | /** 11 | * API class for Tooling API call 12 | * 13 | * @class 14 | * @param {Connection} conn - Connection 15 | */ 16 | var Tooling = function(conn) { 17 | this._conn = conn; 18 | this._logger = conn._logger; 19 | var delegates = [ 20 | "query", 21 | "queryMore", 22 | "create", 23 | "insert", 24 | "retrieve", 25 | "update", 26 | "upsert", 27 | "del", 28 | "delete", 29 | "destroy", 30 | "describe", 31 | "describeGlobal", 32 | "sobject" 33 | ]; 34 | delegates.forEach(function(method) { 35 | this[method] = conn.constructor.prototype[method]; 36 | }, this); 37 | 38 | this.cache = new Cache(); 39 | 40 | var cacheOptions = { 41 | key: function(type) { return type ? "describe." + type : "describe"; } 42 | }; 43 | this.describe$ = this.cache.makeCacheable(this.describe, this, cacheOptions); 44 | this.describe = this.cache.makeResponseCacheable(this.describe, this, cacheOptions); 45 | this.describeSObject$ = this.describe$; 46 | this.describeSObject = this.describe; 47 | 48 | cacheOptions = { key: 'describeGlobal' }; 49 | this.describeGlobal$ = this.cache.makeCacheable(this.describeGlobal, this, cacheOptions); 50 | this.describeGlobal = this.cache.makeResponseCacheable(this.describeGlobal, this, cacheOptions); 51 | 52 | this.initialize(); 53 | }; 54 | 55 | /** 56 | * Initialize tooling API 57 | * @protected 58 | */ 59 | Tooling.prototype.initialize = function() { 60 | this.sobjects = {}; 61 | this.cache.clear(); 62 | this.cache.get('describeGlobal').on('value', _.bind(function(res) { 63 | if (res.result) { 64 | var types = _.map(res.result.sobjects, function(so) { return so.name; }); 65 | _.each(types, this.sobject, this); 66 | } 67 | }, this)); 68 | }; 69 | 70 | /** 71 | * @private 72 | */ 73 | Tooling.prototype._baseUrl = function() { 74 | return this._conn.urls.rest.base + "/tooling"; 75 | }; 76 | 77 | /** 78 | * @private 79 | */ 80 | Tooling.prototype._request = function() { 81 | return this._conn._request.apply(this._conn, arguments); 82 | }; 83 | 84 | /** 85 | * Execute query by using SOQL 86 | * 87 | * @param {String} soql - SOQL string 88 | * @param {Callback.} [callback] - Callback function 89 | * @returns {Query.} 90 | */ 91 | /** 92 | * Query next record set by using query locator 93 | * 94 | * @method Tooling#query 95 | * @param {String} locator - Next record set locator 96 | * @param {Callback.} [callback] - Callback function 97 | * @returns {Query.} 98 | */ 99 | /** 100 | * Retrieve specified records 101 | * 102 | * @method Tooling#queryMore 103 | * @param {String} type - SObject Type 104 | * @param {String|Array.} ids - A record ID or array of record IDs 105 | * @param {Callback.>} [callback] - Callback function 106 | * @returns {Promise.>} 107 | */ 108 | 109 | /** 110 | * Synonym of Tooling#create() 111 | * 112 | * @method Tooling#insert 113 | * @param {String} type - SObject Type 114 | * @param {Object|Array.} records - A record or array of records to create 115 | * @param {Callback.>} [callback] - Callback function 116 | * @returns {Promise.>} 117 | */ 118 | /** 119 | * Create records 120 | * 121 | * @method Tooling#create 122 | * @param {String} type - SObject Type 123 | * @param {Record|Array.} records - A record or array of records to create 124 | * @param {Callback.>} [callback] - Callback function 125 | * @returns {Promise.>} 126 | */ 127 | 128 | /** 129 | * Update records 130 | * 131 | * @method Tooling#update 132 | * @param {String} type - SObject Type 133 | * @param {Record|Array.} records - A record or array of records to update 134 | * @param {Callback.>} [callback] - Callback function 135 | * @returns {Promise.>} 136 | */ 137 | 138 | /** 139 | * Upsert records 140 | * 141 | * @method Tooling#upsert 142 | * @param {String} type - SObject Type 143 | * @param {Record|Array.} records - Record or array of records to upsert 144 | * @param {String} extIdField - External ID field name 145 | * @param {Callback.>} [callback] - Callback 146 | * @returns {Promise.>} 147 | */ 148 | 149 | /** 150 | * Synonym of Tooling#destroy() 151 | * 152 | * @method Tooling#delete 153 | * @param {String} type - SObject Type 154 | * @param {String|Array.} ids - A ID or array of IDs to delete 155 | * @param {Callback.>} [callback] - Callback 156 | * @returns {Promise.>} 157 | */ 158 | /** 159 | * Synonym of Tooling#destroy() 160 | * 161 | * @method Tooling#del 162 | * @param {String} type - SObject Type 163 | * @param {String|Array.} ids - A ID or array of IDs to delete 164 | * @param {Callback.>} [callback] - Callback 165 | * @returns {Promise.>} 166 | */ 167 | /** 168 | * Delete records 169 | * 170 | * @method Tooling#destroy 171 | * @param {String} type - SObject Type 172 | * @param {String|Array.} ids - A ID or array of IDs to delete 173 | * @param {Callback.>} [callback] - Callback 174 | * @returns {Promise.>} 175 | */ 176 | 177 | /** 178 | * Synonym of Tooling#describe() 179 | * 180 | * @method Tooling#describeSObject 181 | * @param {String} type - SObject Type 182 | * @param {Callback.} [callback] - Callback function 183 | * @returns {Promise.} 184 | */ 185 | /** 186 | * Describe SObject metadata 187 | * 188 | * @method Tooling#describe 189 | * @param {String} type - SObject Type 190 | * @param {Callback.} [callback] - Callback function 191 | * @returns {Promise.} 192 | */ 193 | 194 | /** 195 | * Describe global SObjects 196 | * 197 | * @method Tooling#describeGlobal 198 | * @param {Callback.} [callback] - Callback function 199 | * @returns {Promise.} 200 | */ 201 | 202 | /** 203 | * Get SObject instance 204 | * 205 | * @method Tooling#sobject 206 | * @param {String} type - SObject Type 207 | * @returns {SObject} 208 | */ 209 | 210 | /** 211 | * @typedef {Object} Tooling~ExecuteAnonymousResult 212 | * @prop {Boolean} compiled - Flag if the query is compiled successfully 213 | * @prop {String} compileProblem - Error reason in compilation 214 | * @prop {Boolean} success - Flag if the code is executed successfully 215 | * @prop {Number} line - Line number for the error 216 | * @prop {Number} column - Column number for the error 217 | * @prop {String} exceptionMessage - Exception message 218 | * @prop {String} exceptionStackTrace - Exception stack trace 219 | */ 220 | /** 221 | * Executes Apex code anonymously 222 | * 223 | * @param {String} body - Anonymous Apex code 224 | * @param {Callback.} [callback] - Callback function 225 | * @returns {Promise.} 226 | */ 227 | Tooling.prototype.executeAnonymous = function(body, callback) { 228 | var url = this._baseUrl() + "/executeAnonymous?anonymousBody=" + encodeURIComponent(body); 229 | return this._request(url).thenCall(callback); 230 | }; 231 | 232 | /** 233 | * @typedef {Object} Tooling~CompletionsResult 234 | * @prop {Object} publicDeclarations 235 | */ 236 | /** 237 | * Retrieves available code completions of the referenced type 238 | * 239 | * @param {String} [type] - completion type (default 'apex') 240 | * @param {Callback.} [callback] - Callback function 241 | * @returns {Promise.} 242 | */ 243 | Tooling.prototype.completions = function(type, callback) { 244 | if (!_.isString(type)) { 245 | callback = type; 246 | type = 'apex'; 247 | } 248 | var url = this._baseUrl() + "/completions?type=" + encodeURIComponent(type); 249 | return this._request(url).thenCall(callback); 250 | }; 251 | 252 | 253 | module.exports = Tooling; 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Shinichi Tomita ", 3 | "name": "node-salesforce", 4 | "description": "Salesforce API Connection Library for Node.js Applications", 5 | "keywords": [ 6 | "salesforce", 7 | "salesforce.com", 8 | "sfdc", 9 | "force.com", 10 | "database.com" 11 | ], 12 | "homepage": "http://github.com/stomita/node-salesforce", 13 | "version": "0.8.0", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/stomita/node-salesforce.git" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "http://github.com/stomita/node-salesforce/raw/master/LICENSE" 22 | } 23 | ], 24 | "main": "./lib/salesforce", 25 | "scripts": { 26 | "test": "mocha test/*.test.js --require ./test/helper/assert.js --reporter spec" 27 | }, 28 | "engines": { 29 | "node": ">=0.8.0" 30 | }, 31 | "bin": { 32 | "sfjs": "./bin/sfjs", 33 | "sfcoffee": "./bin/sfcoffee" 34 | }, 35 | "dependencies": { 36 | "async": "0.1.x", 37 | "coffee-script": "1.6.x", 38 | "faye": "0.8.x", 39 | "q": "0.9.x", 40 | "request": "2.12.x", 41 | "underscore": "1.4.x", 42 | "xml2js": "0.4.x" 43 | }, 44 | "devDependencies": { 45 | "mocha": "1.15.x", 46 | "power-assert": "0.2.x", 47 | "espower-loader": "0.1.x", 48 | "node-phantom": "0.2.x" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/analytics.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | async = require('async'), 4 | _ = require('underscore'), 5 | fs = require('fs'), 6 | sf = require('../lib/salesforce'), 7 | config = require('./config/salesforce'); 8 | 9 | /** 10 | * 11 | */ 12 | describe("analytics", function() { 13 | 14 | this.timeout(40000); // set timeout to 40 sec. 15 | 16 | var conn = new sf.Connection({ logLevel : config.logLevel }); 17 | 18 | var reportId; 19 | 20 | /** 21 | * 22 | */ 23 | before(function(done) { 24 | conn.login(config.username, config.password, function(err) { 25 | if (err) { return done(err); } 26 | if (!conn.accessToken) { done(new Error("No access token. Invalid login.")); } 27 | conn.sobject('Report').findOne({ Name: 'Lead List Report'}, "Id").execute(function(err, res) { 28 | if (err) { return done(err); } 29 | if (!res) { return done(new Error("No Report Name 'Lead List Report' was found in the org.")); } 30 | reportId = res.Id; 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | /** 37 | * 38 | */ 39 | describe("list recent reports", function() { 40 | it("should return report infomation list", function(done) { 41 | conn.analytics.reports(function(err, reports) { 42 | if (err) { throw err; } 43 | assert.ok(_.isArray(reports)); 44 | reports.forEach(function(report) { 45 | assert.ok(_.isString(report.id)); 46 | assert.ok(_.isString(report.name)); 47 | assert.ok(_.isString(report.url)); 48 | }); 49 | }.check(done)); 50 | }); 51 | }); 52 | 53 | /** 54 | * 55 | */ 56 | describe("describe report", function() { 57 | it("should return report metadata", function(done) { 58 | conn.analytics.report(reportId).describe(function(err, meta) { 59 | if (err) { throw err; } 60 | assert.ok(_.isObject(meta)); 61 | assert.ok(_.isObject(meta.reportMetadata)); 62 | assert.ok(_.isObject(meta.reportTypeMetadata)); 63 | assert.ok(_.isObject(meta.reportExtendedMetadata)); 64 | }.check(done)); 65 | }); 66 | }); 67 | 68 | /** 69 | * 70 | */ 71 | describe("execute report synchronously", function() { 72 | it("should return report execution result", function(done) { 73 | conn.analytics.report(reportId).execute(function(err, result) { 74 | if (err) { throw err; } 75 | assert.ok(_.isObject(result)); 76 | assert.ok(_.isObject(result.reportMetadata)); 77 | assert.ok(result.reportMetadata.id === reportId); 78 | assert.ok(_.isObject(result.factMap)); 79 | assert.ok(_.isObject(result.factMap["T!T"])); 80 | assert.ok(_.isUndefined(result.factMap["T!T"].rows)); 81 | assert.ok(_.isObject(result.factMap["T!T"].aggregates)); 82 | }.check(done)); 83 | }); 84 | }); 85 | 86 | /** 87 | * 88 | */ 89 | describe("execute report synchronously with details", function() { 90 | it("should return report execution result", function(done) { 91 | conn.analytics.report(reportId).execute({ details: true }, function(err, result) { 92 | if (err) { throw err; } 93 | assert.ok(_.isObject(result)); 94 | assert.ok(_.isObject(result.reportMetadata)); 95 | assert.ok(result.reportMetadata.id === reportId); 96 | assert.ok(_.isObject(result.factMap)); 97 | assert.ok(_.isObject(result.factMap["T!T"])); 98 | assert.ok(_.isArray(result.factMap["T!T"].rows)); 99 | assert.ok(_.isObject(result.factMap["T!T"].aggregates)); 100 | }.check(done)); 101 | }); 102 | }); 103 | 104 | /** 105 | * 106 | */ 107 | describe("execute report synchronously with filters overrided", function() { 108 | it("should return report execution result", function(done) { 109 | var metadata = { 110 | reportMetadata : { 111 | reportFilters : [{ 112 | column: 'COMPANY', 113 | operator: 'contains', 114 | value: ',Inc.' 115 | }] 116 | } 117 | }; 118 | conn.analytics.report(reportId).execute({ metadata : metadata, details: true }, function(err, result) { 119 | if (err) { throw err; } 120 | assert.ok(_.isObject(result)); 121 | assert.ok(_.isObject(result.reportMetadata)); 122 | assert.ok(_.isArray(result.reportMetadata.reportFilters)); 123 | assert.ok(result.reportMetadata.id === reportId); 124 | assert.ok(_.isObject(result.factMap)); 125 | assert.ok(_.isObject(result.factMap["T!T"])); 126 | assert.ok(_.isArray(result.factMap["T!T"].rows)); 127 | assert.ok(_.isObject(result.factMap["T!T"].aggregates)); 128 | }.check(done)); 129 | }); 130 | }); 131 | 132 | var instanceId; 133 | 134 | /** 135 | * 136 | */ 137 | describe("execute report asynchronously", function() { 138 | it("should return report instance info", function(done) { 139 | conn.analytics.report(reportId).executeAsync(function(err, instance) { 140 | if (err) { throw err; } 141 | assert.ok(_.isObject(instance)); 142 | assert.ok(_.isString(instance.id)); 143 | assert.ok(_.isString(instance.status)); 144 | assert.ok(_.isString(instance.requestDate)); 145 | instanceId = instance.id; 146 | }.check(done)); 147 | }); 148 | }); 149 | 150 | /** 151 | * 152 | */ 153 | describe("list asynchronously executed report instances", function() { 154 | it("should return report instance list", function(done) { 155 | var rinstance = conn.analytics.report(reportId).instances(function(err, instances) { 156 | if (err) { throw err; } 157 | assert.ok(_.isArray(instances)); 158 | instances.forEach(function(instance) { 159 | assert.ok(_.isString(instance.id)); 160 | assert.ok(_.isString(instance.status)); 161 | assert.ok(_.isString(instance.requestDate)); 162 | }); 163 | }.check(done)); 164 | }); 165 | }); 166 | 167 | /** 168 | * 169 | */ 170 | describe("retrieve asynchronously executed report result", function() { 171 | it("should return report execution result", function(done) { 172 | conn.analytics.report(reportId).instance(instanceId).retrieve(function(err, result) { 173 | if (err) { throw err; } 174 | assert.ok(_.isObject(result)); 175 | assert.ok(_.isObject(result.attributes)); 176 | assert.ok(result.attributes.id === instanceId); 177 | assert.ok(_.isString(result.attributes.status)); 178 | assert.ok(_.isString(result.attributes.requestDate)); 179 | assert.ok(_.isObject(result.reportMetadata)); 180 | assert.ok(result.reportMetadata.id === reportId); 181 | }.check(done)); 182 | }); 183 | }); 184 | 185 | }); 186 | -------------------------------------------------------------------------------- /test/apex.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | async = require('async'), 4 | _ = require('underscore'), 5 | fs = require('fs'), 6 | sf = require('../lib/salesforce'), 7 | config = require('./config/salesforce'); 8 | 9 | /** 10 | * 11 | */ 12 | describe("apex", function() { 13 | 14 | this.timeout(40000); // set timeout to 40 sec. 15 | 16 | var conn = new sf.Connection({ logLevel : config.logLevel }); 17 | 18 | /** 19 | * 20 | */ 21 | before(function(done) { 22 | conn.login(config.username, config.password, function(err) { 23 | if (err) { throw err; } 24 | if (!conn.accessToken) { done(new Error("No access token. Invalid login.")); } 25 | done(); 26 | }); 27 | }); 28 | 29 | var accountId; 30 | 31 | /** 32 | * 33 | */ 34 | describe("post account info", function() { 35 | it("should return created account id", function(done) { 36 | var params = { 37 | name: 'My Apex Rest Test #1', 38 | phone: '654-321-0000', 39 | website: 'http://www.google.com' 40 | }; 41 | conn.apex.post('/MyApexRestTest/', params, function(err, id) { 42 | if (err) { throw err; } 43 | assert.ok(_.isString(id)); 44 | accountId = id; 45 | }.check(done)); 46 | }); 47 | }); 48 | 49 | /** 50 | * 51 | */ 52 | describe("get account info", function() { 53 | it("should return updated account", function(done) { 54 | conn.apex.get('/MyApexRestTest/' + accountId, function(err, acc) { 55 | if (err) { throw err; } 56 | assert.ok(_.isObject(acc)); 57 | assert.ok(acc.Name ==='My Apex Rest Test #1'); 58 | assert.ok(acc.Phone === '654-321-0000'); 59 | assert.ok(acc.Website === 'http://www.google.com'); 60 | }.check(done)); 61 | }); 62 | }); 63 | 64 | /** 65 | * 66 | */ 67 | describe("put account info", function() { 68 | it("should return updated account", function(done) { 69 | var params = { 70 | account: { 71 | Name : 'My Apex Rest Test #1 (put)', 72 | Phone : null 73 | } 74 | }; 75 | conn.apex.put('/MyApexRestTest/' + accountId, params, function(err, acc) { 76 | if (err) { throw err; } 77 | assert.ok(_.isObject(acc)); 78 | assert.ok(acc.Name === 'My Apex Rest Test #1 (put)'); 79 | assert.ok(_.isUndefined(acc.Phone)); 80 | assert.ok(acc.Website === 'http://www.google.com'); 81 | }.check(done)); 82 | }); 83 | }); 84 | 85 | /** 86 | * 87 | */ 88 | describe("patch account info", function() { 89 | it("should return updated account", function(done) { 90 | var params = { 91 | name: 'My Apex Rest Test #1 (patch)' 92 | }; 93 | conn.apex.patch('/MyApexRestTest/' + accountId, params, function(err, acc) { 94 | if (err) { throw err; } 95 | assert.ok(_.isObject(acc)); 96 | assert.ok(acc.Name === 'My Apex Rest Test #1 (patch)'); 97 | assert.ok(_.isUndefined(acc.Phone)); 98 | assert.ok(acc.Website === 'http://www.google.com'); 99 | }.check(done)); 100 | }); 101 | }); 102 | 103 | /** 104 | * 105 | */ 106 | describe("delete account info", function() { 107 | it("should not get any account for delete account id", function(done) { 108 | async.waterfall([ 109 | function(cb) { 110 | conn.apex.delete('/MyApexRestTest/' + accountId, cb); 111 | }, 112 | function(ret, cb) { 113 | conn.sobject('Account').find({ Id: accountId }, cb); 114 | } 115 | ], function(err, records) { 116 | if (err) { throw err; } 117 | assert.ok(records.length === 0); 118 | }.check(done)); 119 | }); 120 | }); 121 | 122 | }); 123 | -------------------------------------------------------------------------------- /test/bulk.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | async = require('async'), 4 | _ = require('underscore'), 5 | fs = require('fs'), 6 | sf = require('../lib/salesforce'), 7 | config = require('./config/salesforce'); 8 | 9 | /** 10 | * 11 | */ 12 | describe("bulk", function() { 13 | 14 | this.timeout(40000); // set timeout to 40 sec. 15 | 16 | var conn = new sf.Connection({ logLevel : config.logLevel }); 17 | 18 | /** 19 | * 20 | */ 21 | before(function(done) { 22 | conn.login(config.username, config.password, function(err) { 23 | if (err) { throw err; } 24 | if (!conn.accessToken) { done(new Error("No access token. Invalid login.")); } 25 | done(); 26 | }); 27 | }); 28 | 29 | /** 30 | * 31 | */ 32 | describe("bulk insert records", function() { 33 | it("should return result status", function(done) { 34 | var records = []; 35 | for (var i=0; i<200; i++) { 36 | records.push({ 37 | Name: 'Bulk Account #'+(i+1), 38 | NumberOfEmployees: 300 * (i+1) 39 | }); 40 | } 41 | records.push({ BillingState: 'CA' }); // should raise error 42 | conn.bulk.load("Account", "insert", records, function(err, rets) { 43 | if (err) { throw err; } 44 | assert.ok(_.isArray(rets)); 45 | var ret; 46 | for (var i=0; i<200; i++) { 47 | ret = rets[i]; 48 | assert.ok(_.isString(ret.id)); 49 | assert.ok(ret.success === true); 50 | } 51 | ret = rets[200]; 52 | assert.ok(_.isNull(ret.id)); 53 | assert.ok(ret.success === false); 54 | }.check(done)); 55 | }); 56 | }); 57 | 58 | /** 59 | * 60 | */ 61 | describe("bulk update", function() { 62 | it("should return updated status", function(done) { 63 | async.waterfall([ 64 | function(next) { 65 | conn.sobject('Account') 66 | .find({ Name : { $like : 'Bulk Account%' }}, { Id: 1, Name : 1 }) 67 | .execute(next); 68 | }, 69 | function(records, next) { 70 | records = records.map(function(rec) { 71 | rec.Name = rec.Name + ' (Updated)'; 72 | return rec; 73 | }); 74 | conn.bulk.load('Account', 'update', records, next); 75 | } 76 | ], function(err, rets) { 77 | if (err) { throw err; } 78 | assert.ok(_.isArray(rets)); 79 | var ret; 80 | for (var i=0; i= 0); 121 | assert.ok(_.isObject(item.likes)); 122 | assert.ok(item.likes.total >= 0); 123 | assert.ok(_.isObject(item.actor)); 124 | assert.ok(_.isString(item.actor.id)); 125 | assert.ok(_.isString(item.actor.type)); 126 | assert.ok(_.isString(item.actor.name)); 127 | assert.ok(_.isString(item.actor.url) && item.url[0] === '/'); 128 | assert.ok(_.isObject(item.actor.photo)); 129 | assert.ok(_.isString(item.actor.photo.url)); 130 | assert.ok(_.isString(item.actor.photo.smallPhotoUrl)); 131 | assert.ok(_.isString(item.actor.photo.largePhotoUrl)); 132 | }); 133 | }.check(done)); 134 | }); 135 | 136 | it("should create new item", function(done) { 137 | conn.chatter.resource("/feeds/news/me/feed-items").create({ 138 | body: { 139 | messageSegments: [{ 140 | type: 'Text', 141 | text: 'This is new post' 142 | }] 143 | } 144 | }, function(err, result) { 145 | if (err) { throw err; } 146 | assert.ok(_.isString(result.id)); 147 | assert.ok(result.type === 'TextPost'); 148 | assert.ok(_.isString(result.url) && result.url[0] === '/'); 149 | assert.ok(_.isObject(result.body)); 150 | feedItemUrl = result.url; 151 | }.check(done)); 152 | }); 153 | 154 | it("should delete feed item", function(done) { 155 | conn.chatter.resource(feedItemUrl).delete(function(err, result) { 156 | if (err) { throw err; } 157 | assert.ok(_.isUndefined(result)); 158 | }.check(done)); 159 | }); 160 | 161 | }); 162 | 163 | /** 164 | * 165 | */ 166 | describe("feed comments", function() { 167 | var feedItemUrl, commentsUrl; 168 | 169 | before(function(done) { 170 | conn.chatter.resource("/feeds/news/me/feed-items").create({ 171 | body: { 172 | messageSegments: [{ 173 | type: 'Text', 174 | text: 'A new post with comments' 175 | }] 176 | } 177 | }, function(err, result) { 178 | if (err) { throw err; } 179 | feedItemUrl = result.url; 180 | commentsUrl = result.comments.currentPageUrl; 181 | }.check(done)); 182 | }); 183 | 184 | it("should create new comment post", function(done) { 185 | conn.chatter.resource(commentsUrl).create({ 186 | body: { 187 | messageSegments: [{ 188 | type: 'Text', 189 | text: 'This is new comment #1' 190 | }] 191 | } 192 | }, function(err, result) { 193 | if (err) { throw err; } 194 | }.check(done)); 195 | }); 196 | 197 | after(function(done) { 198 | conn.chatter.resource(feedItemUrl).delete(function(err, result) { 199 | if (err) { throw err; } 200 | }.check(done)); 201 | }); 202 | 203 | }); 204 | 205 | 206 | /** 207 | * 208 | */ 209 | describe("feed likes", function() { 210 | var feedItemUrl, commentUrl, itemLikesUrl, commentLikesUrl; 211 | 212 | before(function(done) { 213 | conn.chatter.resource("/feeds/news/me/feed-items").create({ 214 | body: { 215 | messageSegments: [{ 216 | type: 'Text', 217 | text: 'A new post with likes' 218 | }] 219 | } 220 | }).then(function(result) { 221 | feedItemUrl = result.url; 222 | itemLikesUrl = result.likes.currentPageUrl; 223 | return conn.chatter.resource(result.comments.currentPageUrl).create({ 224 | body: { 225 | messageSegments: [{ 226 | type: 'Text', 227 | text: 'A new comment with likes' 228 | }] 229 | } 230 | }); 231 | }).thenCall(function(err, result) { 232 | if (err) { throw err; } 233 | commentUrl = result.url; 234 | commentLikesUrl = result.likes.currentPageUrl; 235 | }.check(done)); 236 | }); 237 | 238 | var likeUrl; 239 | 240 | it("should add like to item post", function(done) { 241 | conn.chatter.resource(itemLikesUrl).create(null, function(err, result) { 242 | if (err) { throw err; } 243 | likeUrl = result.url; 244 | }.check(done)); 245 | }); 246 | 247 | it("should remove like from item post", function(done) { 248 | conn.chatter.resource(likeUrl).delete(function(err, result) { 249 | if (err) { throw err; } 250 | }.check(done)); 251 | }); 252 | 253 | it("should add like to comment post", function(done) { 254 | conn.chatter.resource(commentLikesUrl).create(null, function(err, result) { 255 | if (err) { throw err; } 256 | likeUrl = result.url; 257 | }.check(done)); 258 | }); 259 | 260 | it("should remove like from comment post", function(done) { 261 | conn.chatter.resource(likeUrl).delete(function(err, result) { 262 | if (err) { throw err; } 263 | }.check(done)); 264 | }); 265 | 266 | after(function(done) { 267 | conn.chatter.resource(feedItemUrl).delete(function(err, result) { 268 | if (err) { throw err; } 269 | }.check(done)); 270 | }); 271 | 272 | }); 273 | 274 | 275 | /** 276 | * 277 | */ 278 | describe("batch", function() { 279 | var chatter = conn.chatter; 280 | var feeds; 281 | var urls = []; 282 | 283 | before(function(done) { 284 | chatter.resource('/feeds').retrieve(function(err, result) { 285 | if (err) { throw err; } 286 | feeds = result.feeds; 287 | }.check(done)); 288 | }); 289 | 290 | it("should get all feed items", function(done) { 291 | var resources = _.map(feeds, function(feed) { 292 | return chatter.resource(feed.feedItemsUrl); 293 | }); 294 | chatter.batch(resources, function(err, result) { 295 | if (err) { throw err; } 296 | assert.ok(result.hasErrors === false); 297 | assert.ok(_.isArray(result.results) && result.results.length === feeds.length); 298 | _.forEach(result.results, function(result) { 299 | var res = result.result; 300 | assert.ok(_.isString(res.currentPageUrl)); 301 | assert.ok(_.isArray(res.items)); 302 | }); 303 | }.check(done)); 304 | }); 305 | 306 | it("should create new item post and get feed items", function(done) { 307 | chatter.batch([ 308 | chatter.resource('/feeds/news/me/feed-items').create({ 309 | body: { 310 | messageSegments: [{ 311 | type: 'Text', 312 | text: 'This is a post text' 313 | }] 314 | } 315 | }), 316 | chatter.resource('/feeds/news/me/feed-items').create({ 317 | body: { 318 | messageSegments: [{ 319 | type: 'Text', 320 | text: 'This is another post text, following to previous.' 321 | }] 322 | } 323 | }), 324 | chatter.resource('/feeds/news/me/feed-items', { pageSize: 2, sort: "CreatedDateDesc" }), 325 | ], function(err, result) { 326 | assert.ok(result.hasErrors === false); 327 | assert.ok(_.isArray(result.results) && result.results.length === 3); 328 | var item1 = result.results[0].result; 329 | var item2 = result.results[1].result; 330 | var items = result.results[2].result; 331 | urls.push(item1.url); 332 | urls.push(item2.url); 333 | assert.ok(items.items[1].id === item1.id); 334 | assert.ok(items.items[0].id === item2.id); 335 | }.check(done)); 336 | }); 337 | 338 | it("should delete all created resources", function(done) { 339 | if (urls.length > 0) { 340 | chatter.batch(_.map(urls, function(url) { 341 | return chatter.resource(url).delete(); 342 | }), function(err, result) { 343 | if (err) { throw err; } 344 | }.check(done)); 345 | } else { 346 | done(); 347 | } 348 | }); 349 | 350 | }); 351 | 352 | }); 353 | -------------------------------------------------------------------------------- /test/config/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.js.bak 3 | -------------------------------------------------------------------------------- /test/config/oauth2.js.example: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | loginUrl: "https://login.salesforce.com", 3 | clientId : "oauth2 client id is here", 4 | clientSecret : "oauth2 client secret is here", 5 | redirectUri : "http://localhost:4000/oauth2/callback", 6 | username : "username@example.com", 7 | password : "your password is here" 8 | }; 9 | -------------------------------------------------------------------------------- /test/config/salesforce.js.example: -------------------------------------------------------------------------------- 1 | /** 2 | * copy and save this as "salesforce.js", by filling proper information; 3 | */ 4 | module.exports = { 5 | loginServerUrl : "https://login.salesforce.com", 6 | username : "usernamen@example.org", 7 | password : "your password is here" 8 | 9 | clientId : "your oauth2 client id is here", 10 | clientSecret : "your oauth2 client secret is here", 11 | redirectUri : "http://localhost:4000/oauth2/callback", 12 | 13 | // , bigTable : "BigTable__c" 14 | // , upsertTable : "UpsertTable__c" 15 | // , upsertField : "ExtId__c" 16 | }; 17 | -------------------------------------------------------------------------------- /test/connection.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | async = require('async'), 4 | _ = require('underscore'), 5 | authorize = require('./helper/webauth'), 6 | sf = require('../lib/salesforce'), 7 | config = require('./config/salesforce'); 8 | 9 | /** 10 | * 11 | */ 12 | describe("connection", function() { 13 | 14 | this.timeout(40000); // set timeout to 40 sec. 15 | 16 | var conn = new sf.Connection({ logLevel : config.logLevel }); 17 | 18 | /** 19 | * 20 | */ 21 | describe("login", function() { 22 | it("should login by username and password", function(done) { 23 | conn.login(config.username, config.password, function(err, userInfo) { 24 | if (err) { throw err; } 25 | assert.ok(_.isString(conn.accessToken)); 26 | assert.ok(_.isString(userInfo.id)); 27 | assert.ok(_.isString(userInfo.organizationId)); 28 | assert.ok(_.isString(userInfo.url)); 29 | }.check(done)); 30 | }); 31 | }); 32 | 33 | var accountId, account; 34 | /** 35 | * 36 | */ 37 | describe("create account", function() { 38 | it("should return created obj", function(done) { 39 | conn.sobject('Account').create({ Name : 'Hello' }, function(err, ret) { 40 | if (err) { throw err; } 41 | assert.ok(ret.success); 42 | assert.ok(_.isString(ret.id)); 43 | accountId = ret.id; 44 | }.check(done)); 45 | }); 46 | }); 47 | 48 | /** 49 | * 50 | */ 51 | describe("retrieve account", function() { 52 | it("should return a record", function(done) { 53 | conn.sobject('Account').retrieve(accountId, function(err, record) { 54 | if (err) { throw err; } 55 | assert.ok(_.isString(record.Id)); 56 | assert.ok(_.isObject(record.attributes)); 57 | assert.ok(record.Name === 'Hello'); 58 | account = record; 59 | }.check(done)); 60 | }); 61 | }); 62 | 63 | /** 64 | * 65 | */ 66 | describe("update account", function() { 67 | it("should update successfully", function(done) { 68 | conn.sobject('Account').record(account.Id).update({ Name : "Hello2" }, function(err, ret) { 69 | if (err) { throw err; } 70 | assert.ok(ret.success); 71 | }.check(done)); 72 | }); 73 | 74 | describe("then retrieve the account", function() { 75 | it("sholuld return updated account object", function(done) { 76 | conn.sobject('Account').record(accountId).retrieve(function(err, record) { 77 | if (err) { throw err; } 78 | assert.ok(record.Name === 'Hello2'); 79 | assert.ok(_.isObject(record.attributes)); 80 | }.check(done)); 81 | }); 82 | }); 83 | }); 84 | 85 | /** 86 | * 87 | */ 88 | describe("delete account", function() { 89 | it("should delete successfully", function(done) { 90 | conn.sobject('Account').record(account.Id).destroy(function(err, ret) { 91 | if (err) { throw err; } 92 | assert.ok(ret.success); 93 | }.check(done)); 94 | }); 95 | 96 | describe("then retrieve the account", function() { 97 | it("should not return any record for deleted account", function(done) { 98 | conn.sobject('Account').retrieve(account.Id, function(err, record) { 99 | assert.ok(err instanceof Error); 100 | assert.ok(err.errorCode === 'NOT_FOUND'); 101 | }.check(done)); 102 | }); 103 | }); 104 | }); 105 | 106 | 107 | var accountIds, accounts; 108 | /** 109 | * 110 | */ 111 | describe("create multiple accounts", function() { 112 | it("should return created records", function(done) { 113 | conn.sobject('Account').create([ 114 | { Name : 'Account #1' }, 115 | { Name : 'Account #2' } 116 | ], function(err, rets) { 117 | if (err) { throw err; } 118 | assert.ok(_.isArray(rets)); 119 | rets.forEach(function(ret) { 120 | assert.ok(ret.success); 121 | assert.ok(_.isString(ret.id)); 122 | }); 123 | accountIds = rets.map(function(ret){ return ret.id; }); 124 | }.check(done)); 125 | }); 126 | }); 127 | 128 | /** 129 | * 130 | */ 131 | describe("retrieve multiple accounts", function() { 132 | it("should return specified records", function(done) { 133 | conn.sobject('Account').retrieve(accountIds, function(err, records) { 134 | if (err) { throw err; } 135 | assert.ok(_.isArray(records)); 136 | records.forEach(function(record, i) { 137 | assert.ok(_.isString(record.Id)); 138 | assert.ok(_.isObject(record.attributes)); 139 | assert.ok(record.Name === 'Account #' + (i+1)); 140 | }); 141 | accounts = records; 142 | }.check(done)); 143 | }); 144 | }); 145 | 146 | /** 147 | * 148 | */ 149 | describe("update multiple accounts", function() { 150 | it("should update records successfully", function(done) { 151 | conn.sobject('Account').update( 152 | accounts.map(function(account) { 153 | return { Id : account.Id, Name : "Updated " + account.Name }; 154 | }), 155 | function(err, rets) { 156 | if (err) { throw err; } 157 | assert.ok(_.isArray(rets)); 158 | rets.forEach(function(ret){ 159 | assert.ok(ret.success); 160 | }); 161 | }.check(done) 162 | ); 163 | }); 164 | 165 | describe("then retrieve the accounts", function() { 166 | it("sholuld return updated records", function(done) { 167 | conn.sobject('Account').retrieve(accountIds, function(err, records) { 168 | if (err) { throw err; } 169 | assert.ok(_.isArray(records)); 170 | records.forEach(function(record, i) { 171 | assert.ok(record.Name === 'Updated Account #' + (i+1)); 172 | assert.ok(_.isObject(record.attributes)); 173 | }); 174 | }.check(done)); 175 | }); 176 | }); 177 | }); 178 | 179 | /** 180 | * 181 | */ 182 | describe("delete multiple accounts", function() { 183 | it("should delete successfully", function(done) { 184 | conn.sobject('Account').destroy(accountIds, function(err, rets) { 185 | if (err) { throw err; } 186 | assert.ok(_.isArray(rets)); 187 | rets.forEach(function(ret){ 188 | assert.ok(ret.success); 189 | }); 190 | }.check(done)); 191 | }); 192 | 193 | describe("then retrieve the accounts", function() { 194 | it("should not return any records", function(done) { 195 | conn.sobject('Account').retrieve(accountIds, function(err, records) { 196 | assert.ok(err instanceof Error); 197 | assert.ok(err.errorCode === 'NOT_FOUND'); 198 | }.check(done)); 199 | }); 200 | }); 201 | }); 202 | 203 | /** 204 | * 205 | */ 206 | describe("upsert record", function() { 207 | var extId = "ID" + Date.now(); 208 | var recId; 209 | 210 | describe("for not existing record", function() { 211 | it("should create record successfully", function(done) { 212 | var rec = { Name : 'New Record' }; 213 | rec[config.upsertField] = extId; 214 | conn.sobject(config.upsertTable).upsert(rec, config.upsertField, function(err, ret) { 215 | if (err) { throw err; } 216 | assert.ok(ret.success); 217 | assert.ok(_.isString(ret.id)); 218 | recId = ret.id; 219 | }.check(done)); 220 | }); 221 | }); 222 | 223 | describe("for already existing record", function() { 224 | it("should update record successfully", function(done) { 225 | var rec = { Name : 'Updated Record' }; 226 | rec[config.upsertField] = extId; 227 | conn.sobject(config.upsertTable).upsert(rec, config.upsertField, function(err, ret) { 228 | if (err) { throw err; } 229 | assert.ok(ret.success); 230 | assert.ok(_.isUndefined(ret.id)); 231 | }.check(done)); 232 | }); 233 | 234 | describe("then retrieve the record", function() { 235 | it("should return updated record", function(done) { 236 | conn.sobject(config.upsertTable).retrieve(recId, function(err, record) { 237 | if (err) { throw err; } 238 | assert.ok(record.Name === "Updated Record"); 239 | }.check(done)); 240 | }); 241 | }); 242 | }); 243 | 244 | describe("for duplicated external id record", function() { 245 | before(function(done) { 246 | var rec = { Name : 'Duplicated Record' }; 247 | rec[config.upsertField] = extId; 248 | conn.sobject(config.upsertTable).create(rec, done); 249 | }); 250 | 251 | it("should throw error and return array of choices", function(done) { 252 | var rec = { Name : 'Updated Record, Twice' }; 253 | rec[config.upsertField] = extId; 254 | conn.sobject(config.upsertTable).upsert(rec, config.upsertField, function(err, ret) { 255 | assert.ok(err instanceof Error); 256 | assert.ok(err.name === "MULTIPLE_CHOICES"); 257 | assert.ok(_.isArray(err.content)); 258 | assert.ok(_.isString(err.content[0])); 259 | }.check(done)); 260 | }); 261 | }); 262 | }); 263 | 264 | /** 265 | * 266 | */ 267 | describe("describe Account", function() { 268 | it("should return metadata information", function(done) { 269 | conn.sobject('Account').describe(function(err, meta) { 270 | if (err) { throw err; } 271 | assert.ok(meta.name === "Account"); 272 | assert.ok(_.isArray(meta.fields)); 273 | }.check(done)); 274 | }); 275 | 276 | describe("then describe cached Account", function() { 277 | it("should return metadata information", function(done) { 278 | conn.sobject('Account').describe$(function(err, meta) { 279 | if (err) { throw err; } 280 | assert.ok(meta.name === "Account"); 281 | assert.ok(_.isArray(meta.fields)); 282 | }.check(done)); 283 | }); 284 | }); 285 | }); 286 | 287 | /** 288 | * 289 | */ 290 | describe("describe global sobjects", function() { 291 | it("should return whole global sobject list", function(done) { 292 | conn.describeGlobal(function(err, res) { 293 | if (err) { throw err; } 294 | assert.ok(_.isArray(res.sobjects)); 295 | assert.ok(_.isString(res.sobjects[0].name)); 296 | assert.ok(_.isString(res.sobjects[0].label)); 297 | assert.ok(_.isUndefined(res.sobjects[0].fields)); 298 | }.check(done)); 299 | }); 300 | 301 | describe("then describe cached global sobjects", function() { 302 | it("should return whole global sobject list", function(done) { 303 | conn.describeGlobal$(function(err, res) { 304 | if (err) { throw err; } 305 | assert.ok(_.isArray(res.sobjects)); 306 | assert.ok(_.isString(res.sobjects[0].name)); 307 | assert.ok(_.isString(res.sobjects[0].label)); 308 | assert.ok(_.isUndefined(res.sobjects[0].fields)); 309 | }.check(done)); 310 | }); 311 | }); 312 | }); 313 | 314 | /** 315 | * 316 | */ 317 | describe("get updated / deleted account", function () { 318 | before(function(done) { 319 | var accs = [{ Name: 'Hello' }, { Name: 'World' }]; 320 | conn.sobject('Account').create(accs, function(err, rets) { 321 | if (err) { return done(err); } 322 | var id1 = rets[0].id, id2 = rets[1].id; 323 | async.parallel([ 324 | function(cb) { 325 | conn.sobject('Account').record(id1).update({ Name: "Hello2" }, cb); 326 | }, 327 | function(cb) { 328 | conn.sobject('Account').record(id2).destroy(cb); 329 | } 330 | ], function (err, ret) { 331 | if (err) { throw err; } 332 | }.check(done)); 333 | }); 334 | }); 335 | 336 | /** 337 | * 338 | */ 339 | describe("get updated accounts", function () { 340 | it("should return updated account object", function (done) { 341 | var end = new Date(); 342 | var start = new Date(end.getTime() - 2 * 24 * 60 * 60 * 1000); // 2 days before 343 | conn.sobject('Account').updated(start, end, function (err, result) { 344 | if (err) { throw err; } 345 | assert.ok(_.isArray(result.ids)); 346 | }.check(done)); 347 | }); 348 | }); 349 | 350 | /** 351 | * 352 | */ 353 | describe("get updated account with string input", function () { 354 | it("should return updated account object", function (done) { 355 | var end = new Date(); 356 | var start = new Date(end.getTime() - 2 * 24 * 60 * 60 * 1000); // 2 days before 357 | conn.sobject('Account').updated(start.toString(), end.toString(), function (err, result) { 358 | if (err) { throw err; } 359 | assert.ok(_.isArray(result.ids)); 360 | }.check(done)); 361 | }); 362 | }); 363 | 364 | /** 365 | * 366 | */ 367 | describe("get deleted account", function () { 368 | it("should return deleted account object", function (done) { 369 | var end = new Date(); 370 | var start = new Date(end.getTime() - 2 * 24 * 60 * 60 * 1000); // 2 days before 371 | conn.sobject('Account').deleted(start, end, function (err, result) { 372 | if (err) { throw err; } 373 | assert.ok(_.isArray(result.deletedRecords)); 374 | }.check(done)); 375 | }); 376 | }); 377 | 378 | /** 379 | * 380 | */ 381 | describe("get deleted account with string input", function () { 382 | it("should return deleted account object", function (done) { 383 | var end = new Date(); 384 | var start = new Date(end.getTime() - 2 * 24 * 60 * 60 * 1000); // 2 days before 385 | conn.sobject('Account').deleted(start.toString(), end.toString(), function (err, result) { 386 | if (err) { throw err; } 387 | assert.ok(_.isArray(result.deletedRecords)); 388 | }.check(done)); 389 | }); 390 | }); 391 | }); 392 | 393 | 394 | /** 395 | * 396 | */ 397 | describe("logout by soap api", function() { 398 | var sessionInfo; 399 | it("should logout", function(done) { 400 | sessionInfo = { 401 | accessToken : conn.accessToken, 402 | instanceUrl : conn.instanceUrl 403 | }; 404 | conn.logout(function(err) { 405 | if (err) { throw err; } 406 | assert.ok(_.isNull(conn.accessToken)); 407 | }.check(done)); 408 | }); 409 | 410 | describe("then connect with previous session info", function() { 411 | it("should raise authentication error", function(done) { 412 | conn = new sf.Connection(sessionInfo); 413 | setTimeout(function() { // wait a moment 414 | conn.query("SELECT Id FROM User", function(err, res) { 415 | assert.ok(err instanceof Error); 416 | }.check(done)); 417 | }, 5000); 418 | }); 419 | }); 420 | }); 421 | 422 | /** 423 | * 424 | */ 425 | describe("login by oauth2", function() { 426 | var newConn = new sf.Connection({ 427 | oauth2: { 428 | clientId : config.clientId, 429 | clientSecret : config.clientSecret, 430 | redirectUri : config.redirectUri, 431 | }, 432 | logLevel : config.logLevel 433 | }); 434 | 435 | it("should login and get access tokens", function(done) { 436 | async.waterfall([ 437 | function(cb) { 438 | authorize(newConn.oauth2.getAuthorizationUrl(), config.username, config.password, cb); 439 | }, 440 | function(params, cb) { 441 | newConn.authorize(params.code, cb); 442 | } 443 | ], function(err, userInfo) { 444 | if (err) { return done(err); } 445 | assert.ok(_.isString(userInfo.id)); 446 | assert.ok(_.isString(userInfo.organizationId)); 447 | assert.ok(_.isString(userInfo.url)); 448 | assert.ok(_.isString(newConn.accessToken)); 449 | assert.ok(_.isString(newConn.refreshToken)); 450 | }.check(done)); 451 | }); 452 | 453 | describe("then do simple query", function() { 454 | it("should return some records", function(done) { 455 | newConn.query("SELECT Id FROM User", function(err, res) { 456 | assert.ok(_.isArray(res.records)); 457 | }.check(done)); 458 | }); 459 | }); 460 | 461 | describe("then make access token invalid", function() { 462 | var newAccessToken, refreshCount = 0; 463 | it("should return responses", function(done) { 464 | newConn.accessToken = "invalid access token"; 465 | newConn.removeAllListeners("refresh"); 466 | newConn.on("refresh", function(at) { 467 | newAccessToken = at; 468 | refreshCount++; 469 | }); 470 | newConn.query("SELECT Id FROM User", function(err, res) { 471 | assert.ok(refreshCount === 1); 472 | assert.ok(_.isString(newAccessToken)); 473 | assert.ok(_.isArray(res.records)); 474 | }.check(done)); 475 | }); 476 | }); 477 | 478 | describe("then make access token invalid and call api in parallel", function() { 479 | var newAccessToken, refreshCount = 0; 480 | it("should return responses", function(done) { 481 | newConn.accessToken = "invalid access token"; 482 | newConn.removeAllListeners("refresh"); 483 | newConn.on("refresh", function(at) { 484 | newAccessToken = at; 485 | refreshCount++; 486 | }); 487 | async.parallel([ 488 | function(cb) { 489 | newConn.query('SELECT Id FROM User', cb); 490 | }, 491 | function(cb) { 492 | newConn.describeGlobal(cb); 493 | }, 494 | function(cb) { 495 | newConn.sobject('User').describe(cb); 496 | } 497 | ], function(err, results) { 498 | assert.ok(refreshCount === 1); 499 | assert.ok(_.isString(newAccessToken)); 500 | assert.ok(_.isArray(results)); 501 | assert.ok(_.isArray(results[0].records)); 502 | assert.ok(_.isArray(results[1].sobjects)); 503 | assert.ok(_.isArray(results[2].fields)); 504 | }.check(done)); 505 | }); 506 | }); 507 | 508 | describe("then expire both access token and refresh token", function() { 509 | it("should return error response", function(done) { 510 | newConn.accessToken = "invalid access token"; 511 | newConn.refreshToken = "invalid refresh token"; 512 | newConn.query("SELECT Id FROM User", function(err) { 513 | assert.ok(err instanceof Error); 514 | assert.ok(err.name === "invalid_grant"); 515 | }.check(done)); 516 | }); 517 | }); 518 | 519 | }); 520 | 521 | }); 522 | 523 | -------------------------------------------------------------------------------- /test/data/.gitignore: -------------------------------------------------------------------------------- 1 | Account_delete.csv 2 | -------------------------------------------------------------------------------- /test/data/MyPackage.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stomita/node-salesforce/3dec6f206e027f2331cc1ffb9037ac3b50996a3f/test/data/MyPackage.zip -------------------------------------------------------------------------------- /test/helper/assert.js: -------------------------------------------------------------------------------- 1 | require('espower-loader')({ 2 | // directory where match starts with 3 | cwd: process.cwd(), 4 | // glob pattern using minimatch module 5 | pattern: 'test/**/*.test.js' 6 | }); 7 | 8 | /** 9 | * 10 | */ 11 | Function.prototype.check = function(done) { 12 | var fn = this; 13 | return function() { 14 | try { 15 | fn.apply(this, arguments); 16 | return done(); 17 | } catch(e) { 18 | done(e); 19 | throw e; 20 | } 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /test/helper/webauth.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | phantom = require('node-phantom'); 3 | 4 | module.exports = function(url, username, password, callback) { 5 | var ph, page; 6 | async.waterfall([ 7 | function(cb) { 8 | phantom.create(cb); 9 | }, 10 | function(_ph, cb) { 11 | ph = _ph; 12 | ph.createPage(cb); 13 | }, 14 | function(_page, cb) { 15 | page = _page; 16 | page.open(url, cb); 17 | }, 18 | function(status, cb) { 19 | setTimeout(function() { 20 | page.evaluate(function(username, password) { 21 | document.querySelector('#username').value = username; 22 | document.querySelector('#password').value = password; 23 | var e = document.createEvent('MouseEvents'); 24 | e.initEvent("click", false, true); 25 | document.querySelector('button[name=Login]').dispatchEvent(e); 26 | return true; 27 | }, cb, username, password); 28 | }, 4000); 29 | }, 30 | function(status, cb) { 31 | setTimeout(function() { 32 | page.evaluate(function() { 33 | if (location.href.indexOf("RemoteAccessAuthorizationPage.apexp") > 0) { 34 | var e = document.createEvent('MouseEvents'); 35 | e.initEvent("click", false, true); 36 | document.querySelector('#oaapprove').dispatchEvent(e); 37 | return false; 38 | } 39 | return true; 40 | }, cb); 41 | }, 4000); 42 | }, 43 | function(loaded, cb) { 44 | setTimeout(function() { 45 | page.evaluate(function() { 46 | var params = {}; 47 | var m = document.querySelector('head script').innerText.match(/\/callback\?([^'"]+)/); 48 | if (m) { 49 | var qparams = m[1].split('&'); 50 | for (var i=0; i 0); 195 | done(); 196 | }) 197 | .on('error', function(err) { 198 | done(err); 199 | }); 200 | }); 201 | }); 202 | 203 | }); 204 | -------------------------------------------------------------------------------- /test/oauth2.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | _ = require('underscore'), 4 | authorize = require('./helper/webauth'), 5 | OAuth2 = require('../lib/oauth2'), 6 | config = require('./config/oauth2'); 7 | 8 | /** 9 | * 10 | */ 11 | describe("oauth2", function() { 12 | 13 | this.timeout(40000); // set timeout to 40 sec. 14 | 15 | var oauth2 = new OAuth2(config); 16 | 17 | /** 18 | * 19 | */ 20 | describe("OAuth2 web server flow", function() { 21 | var code, accessToken, refreshToken; 22 | 23 | it("should receive authz code", function(done) { 24 | var url = oauth2.getAuthorizationUrl({ state: 'hello' }); 25 | authorize(url, config.username, config.password, function(err, params) { 26 | if (err) { throw err; } 27 | assert.ok(_.isString(params.code)); 28 | assert.ok(params.state === 'hello'); 29 | code = params.code; 30 | }.check(done)); 31 | }); 32 | 33 | it("should receive access/refresh token", function(done) { 34 | oauth2.requestToken(code, function(err, res) { 35 | if (err) { throw err; } 36 | assert.ok(_.isString(res.access_token)); 37 | assert.ok(_.isString(res.refresh_token)); 38 | accessToken = res.access_token; 39 | refreshToken = res.refresh_token; 40 | }.check(done)); 41 | }); 42 | 43 | it("should refresh access token", function(done) { 44 | oauth2.refreshToken(refreshToken, function(err, res) { 45 | if (err) { throw err; } 46 | assert.ok(_.isString(res.access_token)); 47 | }.check(done)); 48 | }); 49 | 50 | }); 51 | 52 | /** 53 | * 54 | */ 55 | describe("OAuth2 username & password flow : authenticate", function() { 56 | it("should receive access token", function(done) { 57 | oauth2.authenticate(config.username, config.password, function(err, res) { 58 | if (err) { throw err; } 59 | assert.ok(_.isString(res.access_token)); 60 | }.check(done)); 61 | }); 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /test/query.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | _ = require('underscore'), 4 | fs = require('fs'), 5 | stream = require('stream'), 6 | Stream = stream.Stream, 7 | querystring = require('querystring'), 8 | sf = require('../lib/salesforce'), 9 | RecordStream = require('../lib/record-stream'), 10 | config = require('./config/salesforce'); 11 | 12 | /** 13 | * 14 | */ 15 | describe("query", function() { 16 | 17 | this.timeout(40000); // set timeout to 40 sec. 18 | 19 | var conn = new sf.Connection({ logLevel : config.logLevel }); 20 | 21 | /** 22 | * 23 | */ 24 | before(function(done) { 25 | conn.login(config.username, config.password, function(err) { 26 | if (err) { throw err; } 27 | if (!conn.accessToken) { done(new Error("No access token. Invalid login.")); } 28 | done(); 29 | }); 30 | }); 31 | 32 | /** 33 | * 34 | */ 35 | describe("query accounts", function() { 36 | it("should return records", function(done) { 37 | var query = conn.query("SELECT Id, Name FROM Account"); 38 | query.run(function(err, result) { 39 | if (err) { throw err; } 40 | assert.ok(_.isNumber(result.totalSize)); 41 | }.check(done)); 42 | }); 43 | }); 44 | 45 | /** 46 | * 47 | */ 48 | describe("query accounts with scanAll option", function() { 49 | before(function(done) { 50 | conn.sobject('Account').create({ Name: 'Deleting Account #1'}, function(err, ret) { 51 | if (err) { return done(err); } 52 | conn.sobject('Account').record(ret.id).destroy(done); 53 | }); 54 | }); 55 | 56 | it("should return records", function(done) { 57 | var query = conn.query("SELECT Id, IsDeleted, Name FROM Account WHERE IsDeleted = true"); 58 | query.run({ scanAll: true }, function(err, result) { 59 | if (err) { throw err; } 60 | assert.ok(_.isNumber(result.totalSize)); 61 | assert.ok(result.totalSize > 0); 62 | }.check(done)); 63 | }); 64 | }); 65 | 66 | /** 67 | * 68 | */ 69 | describe("query big table and execute queryMore", function() { 70 | it("should fetch all records", function(done) { 71 | var records = []; 72 | var handleResult = function(err, res) { 73 | if (err) { callback(err); } 74 | records.push.apply(records, res.records); 75 | if (res.done) { 76 | callback(null, { result: res, records: records }); 77 | } else { 78 | query = conn.queryMore(res.nextRecordsUrl, handleResult); 79 | } 80 | }; 81 | var query = conn.query("SELECT Id, Name FROM " + (config.bigTable || 'Account'), handleResult); 82 | var callback = function(err, result) { 83 | if (err) { throw err; } 84 | assert.equal(result.records.length, result.result.totalSize); 85 | }.check(done); 86 | }); 87 | }); 88 | 89 | /** 90 | * 91 | */ 92 | describe("query big tables without autoFetch", function() { 93 | describe("should scan records in one query fetch", function(done) { 94 | var records = []; 95 | var query = conn.query("SELECT Id, Name FROM " + (config.bigTable || 'Account')); 96 | query.on('record', function(record, i, cnt){ 97 | records.push(record); 98 | }); 99 | query.on('end', function() { 100 | callback(null, { query : query, records : records }); 101 | }); 102 | query.on('error', function(err) { 103 | callback(err); 104 | }); 105 | query.run({ autoFetch : false }); 106 | var callback = function(err, result) { 107 | if (err) { throw err; } 108 | assert.ok(result.query.totalFetched === result.records.length); 109 | assert.ok(result.query.totalSize > 2000 ? 110 | result.query.totalFetched === 2000 : 111 | result.query.totalFetched === result.query.totalSize 112 | ); 113 | }.check(done); 114 | }); 115 | }); 116 | 117 | /** 118 | * 119 | */ 120 | describe("query big tables with autoFetch", function() { 121 | it("should scan records up to maxFetch num", function(done) { 122 | var records = []; 123 | var query = conn.query("SELECT Id, Name FROM " + (config.bigTable || 'Account')); 124 | query.on('record', function(record) { 125 | records.push(record); 126 | }) 127 | .on('error', function(err) { 128 | callback(err); 129 | }) 130 | .on('end', function() { 131 | callback(null, { query : query, records : records }); 132 | }) 133 | .run({ autoFetch : true, maxFetch : 5000 }); 134 | var callback = function(err, result) { 135 | if (err) { throw err; } 136 | assert.ok(result.query.totalFetched === result.records.length); 137 | assert.ok(result.query.totalSize > 5000 ? 138 | result.query.totalFetched === 5000 : 139 | result.query.totalFetched === result.query.totalSize 140 | ); 141 | }.check(done); 142 | }); 143 | }); 144 | 145 | /** 146 | * 147 | */ 148 | describe("query big tables by piping randomly-waiting output record stream object", function() { 149 | it("should scan records via stream up to maxFetch num", function(done) { 150 | var records = []; 151 | var query = conn.query("SELECT Id, Name FROM " + (config.bigTable || 'Account')); 152 | var outStream = new RecordStream(); 153 | outStream.sendable = true; 154 | outStream.send = function(record) { 155 | records.push(record); 156 | if (records.length % 100 === 0) { 157 | outStream.sendable = false; 158 | setTimeout(function() { 159 | outStream.sendable = true; 160 | outStream.emit('drain'); 161 | }, 1000 * Math.random()); 162 | } 163 | return outStream.sendable; 164 | }; 165 | outStream.end = function() { 166 | callback(null, { query : query, records : records }); 167 | }; 168 | query.pipe(outStream); 169 | query.on("error", function(err) { callback(err); }); 170 | 171 | var callback = function(err, result) { 172 | if (err) { throw err; } 173 | assert.ok(result.query.totalFetched === result.records.length); 174 | assert.ok(result.query.totalSize > 5000 ? 175 | result.query.totalFetched === 5000 : 176 | result.query.totalFetched === result.query.totalSize 177 | ); 178 | }.check(done); 179 | }); 180 | }); 181 | 182 | /** 183 | * 184 | */ 185 | describe("query table and convert to readable stream", function() { 186 | it("should get CSV text", function(done) { 187 | var query = conn.query("SELECT Id, Name FROM Account LIMIT 10"); 188 | var csvOut = new Stream(); 189 | csvOut.writable = true; 190 | var result = ''; 191 | csvOut.write = function(data) { 192 | result += data; 193 | }; 194 | csvOut.end = function(data) { 195 | result += data; 196 | csvOut.writable = false; 197 | callback(null, result); 198 | }; 199 | query.stream().pipe(csvOut); 200 | var callback = function(err, csv) { 201 | if (err) { throw err; } 202 | assert.ok(_.isString(csv)); 203 | var header = csv.split("\n")[0]; 204 | assert.equal(header, "Id,Name"); 205 | }.check(done); 206 | }); 207 | }); 208 | 209 | }); 210 | 211 | -------------------------------------------------------------------------------- /test/sobject.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | _ = require('underscore'), 4 | async = require('async'), 5 | sf = require('../lib/salesforce'), 6 | SObject = require("../lib/sobject"), 7 | config = require('./config/salesforce'); 8 | 9 | /** 10 | * 11 | */ 12 | describe("sobject", function() { 13 | 14 | this.timeout(40000); // set timeout to 40 sec. 15 | 16 | var conn = new sf.Connection({ logLevel : config.logLevel }); 17 | 18 | var Account, Opportunity; 19 | /** 20 | * 21 | */ 22 | before(function(done) { 23 | conn.login(config.username, config.password, function(err) { 24 | if (err) { throw err; } 25 | if (!conn.accessToken) { done(new Error("No access token. Invalid login.")); } 26 | done(); 27 | }); 28 | }); 29 | 30 | /** 31 | * 32 | */ 33 | describe("create sobject", function() { 34 | it("should get SObject instances", function() { 35 | Account = conn.sobject("Account"); 36 | Opportunity = conn.sobject("Opportunity"); 37 | assert.ok(Account instanceof SObject); 38 | assert.ok(Opportunity instanceof SObject); 39 | }); 40 | }); 41 | 42 | var acc; 43 | /** 44 | * 45 | */ 46 | describe("find records", function() { 47 | 48 | it("should return records", function(done) { 49 | Account.find().run(function(err, records) { 50 | if (err) { throw err; } 51 | assert.ok(_.isArray(records)); 52 | }.check(done)); 53 | }); 54 | 55 | it("should return records with direct callback", function(done) { 56 | Account.find({}, { Name : 1 }, function(err, records) { 57 | if (err) { throw err; } 58 | assert.ok(_.isArray(records)); 59 | acc = records[0]; // keep sample account record 60 | }.check(done)); 61 | }); 62 | 63 | it("should return records with conditions", function(done) { 64 | var likeStr = acc.Name[0] + "%"; 65 | Account.find({ Name : { $like : likeStr } }, { Name : 1 }, function(err, records) { 66 | if (err) { throw err; } 67 | assert.ok(_.isArray(records)); 68 | assert.ok(records.length > 0); 69 | }.check(done)); 70 | }); 71 | }); 72 | 73 | /** 74 | * 75 | */ 76 | describe("find one record", function() { 77 | it("should return a record", function(done) { 78 | Account.findOne({ Id : acc.Id }, function(err, record) { 79 | if (err) { throw err; } 80 | assert.ok(_.isObject(record)); 81 | assert.ok(_.isString(record.Id)); 82 | }.check(done)); 83 | }); 84 | }); 85 | 86 | /** 87 | * 88 | */ 89 | describe("count records", function() { 90 | it("should return total size count", function(done) { 91 | var likeStr = acc.Name[0] + "%"; 92 | Account.count({ Name : { $like : likeStr } }, function(err, count) { 93 | if (err) { throw err; } 94 | assert.ok(_.isNumber(count)); 95 | assert.ok(count > 0); 96 | }.check(done)); 97 | }); 98 | }); 99 | 100 | /** 101 | * 102 | */ 103 | describe("find records with sort option", function() { 104 | it("should return sorted records", function(done) { 105 | Opportunity.find({}, { CloseDate : 1 }) 106 | .sort("CloseDate", "desc") 107 | .exec(function(err, records) { 108 | if (err) { throw err; } 109 | assert.ok(_.isArray(records)); 110 | assert.ok(records.length > 0); 111 | for (var i=0; i= records[i+1].CloseDate); 113 | } 114 | }.check(done)); 115 | }); 116 | }); 117 | 118 | /** 119 | * 120 | */ 121 | describe("find records with multiple sort options", function() { 122 | it("should return sorted records", function(done) { 123 | Opportunity.find({}, { "Account.Name" : 1, CloseDate : 1 }) 124 | .sort("Account.Name -CloseDate") 125 | .exec(function(err, records) { 126 | if (err) { throw err; } 127 | assert.ok(_.isArray(records)); 128 | assert.ok(records.length > 0); 129 | for (var i=0; i= r2.CloseDate); 134 | } 135 | } 136 | }.check(done)); 137 | }); 138 | }); 139 | 140 | /** 141 | * 142 | */ 143 | describe("find records with multiple sort options and limit option", function() { 144 | it("should return sorted records", function(done) { 145 | Opportunity.find({}, { "Owner.Name" : 1, CloseDate : 1 }) 146 | .sort({ "Owner.Name" : 1, CloseDate : -1 }) 147 | .limit(10) 148 | .exec(function(err, records) { 149 | if (err) { throw err; } 150 | assert.ok(_.isArray(records)); 151 | assert.ok(records.length > 0); 152 | assert.ok(records.length < 11); 153 | for (var i=0; i= r2.CloseDate); 158 | } 159 | } 160 | }.check(done)); 161 | }); 162 | }); 163 | 164 | /** 165 | * 166 | */ 167 | describe("select records", function() { 168 | it("should return records", function(done) { 169 | Opportunity.select("Id,Owner.Name,CloseDate") 170 | .limit(10) 171 | .exec(function(err, records) { 172 | if (err) { throw err; } 173 | assert.ok(_.isArray(records)); 174 | assert.ok(records.length > 0); 175 | assert.ok(records.length < 11); 176 | for (var i=0; i 0); 222 | assert.ok(records.length < 11); 223 | for (var i=0; i 0); 232 | assert.ok(crecords.length < 3); 233 | for (var j=0; j 0); 245 | assert.ok(orecords.length < 3); 246 | for (var k=0; k= 1000 AND Amount < 2000)" 102 | ); 103 | }); 104 | }); 105 | 106 | /** 107 | * 108 | */ 109 | describe("Query with nested NOT/AND operator", function() { 110 | var soql = SOQLBuilder.createSOQL({ 111 | table: "Opportunity", 112 | conditions: { 113 | $not : { 114 | $and : [ 115 | { Amount: { $gte : 1000 } }, 116 | { Amount: { $lt : 2000 } }, 117 | { 'Account.Type' : 'Customer' } 118 | ] 119 | } 120 | } 121 | }); 122 | 123 | it("should equal to soql", function() { 124 | assert.ok(soql === 125 | "SELECT Id FROM Opportunity " + 126 | "WHERE NOT (Amount >= 1000 AND Amount < 2000 AND Account.Type = 'Customer')" 127 | ); 128 | }); 129 | }); 130 | 131 | /** 132 | * 133 | */ 134 | describe("Query with nested NOT operator", function() { 135 | var soql = SOQLBuilder.createSOQL({ 136 | table: "Opportunity", 137 | conditions: { 138 | $not : { 139 | Name: { $like : 'Test%' } 140 | }, 141 | Amount: { $gte: 1000 } 142 | } 143 | }); 144 | 145 | it("should equal to soql", function() { 146 | assert.ok(soql === 147 | "SELECT Id FROM Opportunity " + 148 | "WHERE (NOT Name LIKE 'Test%') AND Amount >= 1000" 149 | ); 150 | }); 151 | }); 152 | 153 | /** 154 | * 155 | */ 156 | describe("Query with nested OR/NOT/AND operator", function() { 157 | var soql = SOQLBuilder.createSOQL({ 158 | table: "Opportunity", 159 | conditions: { 160 | $or : [ 161 | { 'Account.Type' : 'Partner' }, 162 | { 163 | $not : { 164 | $and : [ 165 | { Amount: { $gte : 1000 } }, 166 | { Amount: { $lt : 2000 } }, 167 | { 'Account.Type' : 'Customer' } 168 | ] 169 | } 170 | } 171 | ] 172 | } 173 | }); 174 | 175 | it("should equal to soql", function() { 176 | assert.ok(soql === 177 | "SELECT Id FROM Opportunity " + 178 | "WHERE Account.Type = 'Partner' OR (NOT (Amount >= 1000 AND Amount < 2000 AND Account.Type = 'Customer'))" 179 | ); 180 | }); 181 | }); 182 | 183 | /** 184 | * 185 | */ 186 | describe("Query with Date field for date literal", function() { 187 | var soql = SOQLBuilder.createSOQL({ 188 | table: "Opportunity", 189 | conditions: { 190 | $and : [ 191 | { CloseDate: { $gte : SfDate.LAST_N_DAYS(10) } }, 192 | { CloseDate: { $lte : SfDate.TOMORROW } }, 193 | { CloseDate: { $gt : SfDate.toDateLiteral(new Date(1288958400000)) }}, 194 | { CreatedDate: { $lt : SfDate.toDateTimeLiteral('2010-11-02T04:45:04+09:00') }} 195 | ] 196 | } 197 | }); 198 | 199 | it("should equal to soql", function() { 200 | assert.ok(soql === 201 | "SELECT Id FROM Opportunity " + 202 | "WHERE CloseDate >= LAST_N_DAYS:10 AND CloseDate <= TOMORROW " + 203 | "AND CloseDate > 2010-11-05 AND CreatedDate < 2010-11-01T19:45:04Z" 204 | ); 205 | }); 206 | }); 207 | 208 | /** 209 | * 210 | */ 211 | describe("Query with String field using $like/$nlike operator", function() { 212 | var soql = SOQLBuilder.createSOQL({ 213 | table: "Account", 214 | conditions: { 215 | Name : { $like : "John's%"}, 216 | 'Owner.Name' : { $nlike : '%Test%' } 217 | } 218 | }); 219 | 220 | it("should equal to soql", function() { 221 | assert.ok(soql === 222 | "SELECT Id FROM Account " + 223 | "WHERE Name LIKE 'John\\'s%' AND (NOT Owner.Name LIKE '%Test%')" 224 | ); 225 | }); 226 | }); 227 | 228 | /** 229 | * 230 | */ 231 | describe("Query using $in/$nin operator", function() { 232 | var soql = SOQLBuilder.createSOQL({ 233 | table: "Contact", 234 | conditions: { 235 | "Id" : { $in : [] }, 236 | "Account.Id" : { $in : [ '0011000000NPNrW', '00110000005WlZd' ] }, 237 | "Owner.Id" : { $nin : [ '00510000000N2C2' ] } 238 | } 239 | }); 240 | 241 | it("should equal to soql", function() { 242 | assert.ok(soql === 243 | "SELECT Id FROM Contact " + 244 | "WHERE Account.Id IN ('0011000000NPNrW', '00110000005WlZd') "+ 245 | "AND Owner.Id NOT IN ('00510000000N2C2')" 246 | ); 247 | }); 248 | }); 249 | 250 | /** 251 | * 252 | */ 253 | describe("Query using $exists operator", function() { 254 | var soql = SOQLBuilder.createSOQL({ 255 | table: "Task", 256 | conditions: { 257 | WhatId: { $exists: true }, 258 | WhoId: { $exists: false } 259 | } 260 | }); 261 | 262 | it("should equal to soql", function() { 263 | assert.ok(soql === 264 | "SELECT Id FROM Task " + 265 | "WHERE WhatId != null AND WhoId = null" 266 | ); 267 | }); 268 | }); 269 | 270 | /** 271 | * 272 | */ 273 | describe("Query for matching null", function() { 274 | var soql = SOQLBuilder.createSOQL({ 275 | table: "Account", 276 | conditions: { 277 | Type: { $ne: null }, 278 | LastActivityDate: null 279 | } 280 | }); 281 | 282 | it("should equal to soql", function() { 283 | assert.ok(soql === 284 | "SELECT Id FROM Account " + 285 | "WHERE Type != null AND LastActivityDate = null" 286 | ); 287 | }); 288 | }); 289 | 290 | /** 291 | * 292 | */ 293 | describe("Query with undefined condition", function() { 294 | var soql = SOQLBuilder.createSOQL({ 295 | table: "Account", 296 | conditions: { 297 | Type : undefined 298 | } 299 | }); 300 | 301 | it("should equal to soql", function() { 302 | assert.ok(soql === "SELECT Id FROM Account"); 303 | }); 304 | }); 305 | 306 | /** 307 | * 308 | */ 309 | describe("Query with sort option", function() { 310 | var soql = SOQLBuilder.createSOQL({ 311 | table: "Opportunity", 312 | sort: "-CreatedDate", 313 | limit : 10 314 | }); 315 | 316 | it("should equal to soql", function() { 317 | assert.ok(soql === 318 | "SELECT Id FROM Opportunity " + 319 | "ORDER BY CreatedDate DESC " + 320 | "LIMIT 10" 321 | ); 322 | }); 323 | }); 324 | 325 | /** 326 | * 327 | */ 328 | describe("Query with multiple sort option in array", function() { 329 | var soql = SOQLBuilder.createSOQL({ 330 | table: "Opportunity", 331 | conditions: { 332 | "Owner.Name" : { $like : "A%" } 333 | }, 334 | sort: [ 335 | [ "CreatedDate", "desc" ], 336 | [ "Name", "asc" ] 337 | ], 338 | limit : 10 339 | }); 340 | 341 | it("should equal to soql", function() { 342 | assert.ok(soql === 343 | "SELECT Id FROM Opportunity " + 344 | "WHERE Owner.Name LIKE 'A%' " + 345 | "ORDER BY CreatedDate DESC, Name ASC " + 346 | "LIMIT 10" 347 | ); 348 | }); 349 | }); 350 | 351 | /** 352 | * 353 | */ 354 | describe("Query with multiple sort option in hash", function() { 355 | var soql = SOQLBuilder.createSOQL({ 356 | table: "Opportunity", 357 | conditions: { 358 | "Owner.Name" : { $like : "A%" } 359 | }, 360 | sort: { 361 | CreatedDate: "descending", 362 | Name : "ascending" 363 | }, 364 | limit : 10 365 | }); 366 | 367 | it("should equal to soql", function() { 368 | assert.ok(soql === 369 | "SELECT Id FROM Opportunity " + 370 | "WHERE Owner.Name LIKE 'A%' " + 371 | "ORDER BY CreatedDate DESC, Name ASC " + 372 | "LIMIT 10" 373 | ); 374 | }); 375 | }); 376 | 377 | }); 378 | -------------------------------------------------------------------------------- /test/streaming.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | _ = require('underscore'), 4 | sf = require('../lib/salesforce'), 5 | config = require('./config/streaming'); 6 | 7 | 8 | /** 9 | * 10 | */ 11 | describe("streaming", function() { 12 | 13 | this.timeout(40000); // set timeout to 40 sec. 14 | 15 | var conn = new sf.Connection({ logLevel : config.logLevel }); 16 | 17 | /** 18 | * 19 | */ 20 | before(function(done) { 21 | conn.login(config.username, config.password, function(err) { 22 | if (err) { done(err); } 23 | if (!conn.accessToken) { done(new Error("No access token. Invalid login.")); } 24 | done(); 25 | }); 26 | }); 27 | 28 | /** 29 | * 30 | */ 31 | describe("subscribe to topic and create account", function() { 32 | it("should receive event account created", function(done) { 33 | var listener = function(msg) { 34 | assert.equal("created", msg.event.type); 35 | assert.ok(typeof msg.sobject.Name === 'string'); 36 | assert.ok(typeof msg.sobject.Id === 'string'); 37 | }.check(done); 38 | conn.streaming.topic(config.pushTopicName).subscribe(listener); 39 | // wait 5 secs for subscription complete 40 | setTimeout(function() { 41 | conn.sobject('Account').create({ 42 | Name: 'My New Account #'+Date.now() 43 | }, function() {}); 44 | }, 5000); 45 | }); 46 | }); 47 | 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /test/tooling.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before */ 2 | var assert = require('power-assert'), 3 | async = require('async'), 4 | _ = require('underscore'), 5 | fs = require('fs'), 6 | sf = require('../lib/salesforce'), 7 | config = require('./config/salesforce'); 8 | 9 | /** 10 | * 11 | */ 12 | describe("tooling", function() { 13 | 14 | this.timeout(40000); // set timeout to 40 sec. 15 | 16 | var conn = new sf.Connection({ logLevel : config.logLevel }); 17 | 18 | /** 19 | * 20 | */ 21 | before(function(done) { 22 | conn.login(config.username, config.password, function(err) { 23 | if (err) { return done(err); } 24 | if (!conn.accessToken) { done(new Error("No access token. Invalid login.")); } 25 | done(); 26 | }); 27 | }); 28 | 29 | /** 30 | * 31 | */ 32 | describe("execute anonymous apex", function() { 33 | it("should execute successfully", function(done) { 34 | var body = [ 35 | "System.debug('Hello, World');" 36 | ].join('\n'); 37 | conn.tooling.executeAnonymous(body, function(err, res) { 38 | if (err) { throw err; } 39 | assert.ok(res.compiled === true); 40 | assert.ok(res.success === true); 41 | }.check(done)); 42 | }); 43 | }); 44 | 45 | /** 46 | * 47 | */ 48 | describe("get completions", function() { 49 | this.timeout(40000); // set timeout to 40 sec, because it tends to be long-time query 50 | 51 | it("should return completions", function(done) { 52 | conn.tooling.completions("apex", function(err, res) { 53 | if (err) { throw err; } 54 | assert.ok(_.isObject(res)); 55 | assert.ok(_.isObject(res.publicDeclarations)); 56 | }.check(done)); 57 | }); 58 | }); 59 | 60 | }); 61 | --------------------------------------------------------------------------------