salesforce/lib/connection.js (209 lines of code) (raw):

// Copyright Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. "use strict"; const request = require("request-promise"); const Logger = require("./logger"); const Query = require("./query"); const Chatter = require("./chatter"); const { parseJSON, parseXML, parseText, esc } = require("./helper"); const defaults = { loginUrl: "https://login.salesforce.com", instanceUrl: "", version: "42.0" }; /** * Connection class to cache the session info of the logged-in user * @public * @constructor * @param {Object} [options] - Connection options * @param {String} [options.logLevel] - Output logging level (DEBUG|INFO|WARN|ERROR|FATAL) * @param {String} [options.version] - Salesforce API Version (without "v" prefix) * @param {Number} [options.maxRequest] - Max number of requests allowed in parallel call * @param {String} [options.loginUrl] - Salesforce Login Server URL (e.g. https://login.salesforce.com/) * @param {String} [options.instanceUrl] - Salesforce Instance URL (instance of your Salesforce organization is on e.g. https://na1.salesforce.com/) * @param {String} [options.serverUrl] - Salesforce SOAP service endpoint URL (e.g. https://na1.salesforce.com/services/Soap/u/28.0) * @param {String} [options.sessionId] - Salesforce session ID */ class Connection { constructor(options) { this.options = options || {}; this.logger = new Logger(this.options.logLevel); this.loginUrl = this.options.loginUrl || defaults.loginUrl; this.version = this.options.version || defaults.version; this.maxRequest = this.options.maxRequest || this.maxRequest || 10; this.initialize(this.options); } /** * Initialize connection. * * @protected * @param {Object} options - Initialization options * @param {String} [options.instanceUrl] - Salesforce Instance URL (e.g. https://na1.salesforce.com/) * @param {String} [options.serverUrl] - Salesforce SOAP service endpoint URL (e.g. https://na1.salesforce.com/services/Soap/u/28.0) * @param {String} [options.sessionId] - Salesforce session ID * @param {UserInfo} [options.userInfo] - Logged in user information */ initialize(options) { if (!options.instanceUrl && options.serverUrl) { options.instanceUrl = options.serverUrl.split("/").slice(0, 3).join("/"); } if (options.userInfo) { this.userInfo = options.userInfo; } this.instanceUrl = options.instanceUrl || options.serverUrl || this.instanceUrl || defaults.instanceUrl; this.accessToken = options.sessionId || this.accessToken; this.collectionObjects = {}; } /** * Parse response body * @protected */ parseResponseBody(response) { let contentType = response.headers && response.headers["content-type"]; let parseBody = /^(text|application)\/xml(;|$)/.test(contentType) ? parseXML : /^application\/json(;|$)/.test(contentType) ? parseJSON : parseText; try { return parseBody(response.body); } catch (e) { return response.body; } } /** * Get response body * @protected */ getResponseBody(response) { if (response.statusCode === 204) { return "NO_CONTENT_RESPONSE"; } let body = this.parseResponseBody(response); if (response.statusCode === 300) { let err = new Error("Multiple records found"); err.name = "MULTIPLE_CHOICES"; err.content = body; throw err; } return body; } /** * Gives base url * @public */ baseUrl() { return [this.instanceUrl, "services/data", "v" + this.version].join("/"); } /** * Make HTTP Request call with authorization scopes of the current logged-in session * @public * @param {String|Object} options - HTTP request object or URL to GET request * @param {String} options.method - HTTP method URL to send HTTP request * @param {String} options.url - URL to send HTTP request * @param {Object} [options.headers] - HTTP request headers in hash object (key-value) * @returns {Promise.<Object>} */ request(options) { if (typeof (options) === "string") { options = { method: "GET", url: request }; } if (options.url[0] === "/") { if (options.url.indexOf("/services/") === 0) { options.url = this.instanceUrl + options.url; } else { options.url = this.baseUrl() + options.url; } } options.headers = options.headers || {}; if (this.accessToken) { options.headers.Authorization = "Bearer " + this.accessToken; } if (this.callOptions) { let callOptions = []; for (let name in this.callOptions) { callOptions.push(name + "=" + this.callOptions[name]); } options.headers["Sforce-Call-Options"] = callOptions.join(", "); } options.resolveWithFullResponse = true; this.logger.debug("<request> method=" + options.method + ", url=" + options.url); let requestTime = Date.now(); return request(options) .then((response) => { let responseTime = Date.now(); this.logger.debug("elappsed time : " + (responseTime - requestTime) + "msec"); this.logger.debug("<response> status=" + response.statusCode + ", url=" + request.url); return this.getResponseBody(response); }) .catch((error) => { let responseTime = Date.now(); this.logger.debug("elappsed time : " + (responseTime - requestTime) + "msec"); this.logger.error(error); let err = new Error(error.message); err.name = error.errorCode; for (let key in error) { err[key] = error[key]; } throw err; }); } /** * Login by SOAP web service API * @public * @param {Object} options - Salesforce connection object * @param {String} options.username - Salesforce password (and security token, if required) * @param {String} options.password - Salesforce password (and security token, if required) * @param {String} options.security_token - Salesforce Security Token * @returns {Promise.<UserInfo>} */ login(options) { let body = [ "<se:Envelope xmlns:se='http://schemas.xmlsoap.org/soap/envelope/'>", "<se:Header/>", "<se:Body>", "<login xmlns='urn:partner.soap.sforce.com'>", "<username>" + esc(options.username) + "</username>", "<password>" + esc([options.password, options.security_token].join("")) + "</password>", "</login>", "</se:Body>", "</se:Envelope>" ].join(""); let soapLoginEndpoint = [this.loginUrl, "services/Soap/u", this.version].join("/"); return request({ method: "POST", url: soapLoginEndpoint, body: body, headers: { "Content-Type": "text/xml", "SOAPAction": '""' }, resolveWithFullResponse: true }).then((response) => { this.logger.debug("SOAP response = " + response.body); let parsedResponse = parseXML(response.body); let responseBody = parsedResponse["soapenv:Envelope"]["soapenv:Body"]; let { serverUrl, sessionId, userId, userInfo: { organizationId } } = responseBody.loginResponse.result; let idUrl = soapLoginEndpoint.split("/").slice(0, 3).join("/") + "/id/" + organizationId + "/" + userId; let userInfo = { id: userId, organizationId: organizationId, url: idUrl }; this.initialize({ serverUrl: serverUrl.split("/").slice(0, 3).join("/"), sessionId: sessionId, userInfo: userInfo }); this.logger.debug("<login> completed. user id = " + userId + ", org id = " + organizationId); return userInfo; }) .catch(err => { this.logger.debug("<login> error. error details = " + err); const error = err.error.match(/<faultstring>([^<]+)<\/faultstring>/); const faultstring = error && error[1]; throw new Error(faultstring || err.message); }); } /** * Logout the session by using SOAP web service API * @public * @returns {Promise} */ logout() { let body = [ "<se:Envelope xmlns:se='http://schemas.xmlsoap.org/soap/envelope/'>", "<se:Header>", "<SessionHeader xmlns='urn:partner.soap.sforce.com'>", "<sessionId>" + esc(this.accessToken) + "</sessionId>", "</SessionHeader>", "</se:Header>", "<se:Body>", "<logout xmlns='urn:partner.soap.sforce.com'/>", "</se:Body>", "</se:Envelope>" ].join(""); return request({ method: "POST", url: [this.instanceUrl, "services/Soap/u", this.version].join("/"), body: body, headers: { "Content-Type": "text/xml", "SOAPAction": '""' }, resolveWithFullResponse: true }).then((response) => { this.logger.debug("SOAP statusCode = " + response.statusCode + ", response = " + response.body); this.accessToken = null; this.userInfo = null; this.instanceUrl = null; return undefined; }).catch(err => { throw new Error(err); }); } /** * Execute query by using SOQL * @public * @param {String} soql - SOQL string * @param {Object} [options] - Query options * @param {Object} [options.headers] - Additional HTTP request headers sent in query request * @param {Callback.<QueryResult>} [callback] - Callback function * @returns {Query.<QueryResult>} */ query(soql, options, callback) { if (typeof options === "function") { callback = options; options = undefined; } const query = new Query(this, soql, options); if (callback) { return query.run({}) .then((response) => { callback(null, response); }); } return query; } /**Collection - Accepts table name as input and passses it on to Query as config.table * @public * @param {String} name */ collection(name) { if (!this.collectionObjects[name]) { this.collectionObjects[name] = new Query(this, { table: name }, {}); } return this.collectionObjects[name]; } /** * Return the definition of the collection. * @public * @param {String} name Name of the table/collection. * @returns {Object} Collection Instance. */ getCollectionDetails(name) { return this.request({ url: `${this.baseUrl()}/sobjects/${name}/describe`, method: "GET", json: true }); } /** * Chatter- Passes the instance of connection to chatter * @public */ chatter() { return new Chatter(this, {}); } } module.exports = Connection;