Source: soap.js

  1. /**
  2. * @file Manages method call to SOAP endpoint
  3. * @author Shinichi Tomita <shinichi.tomita@gmail.com>
  4. */
  5. 'use strict';
  6. var inherits = require('inherits'),
  7. _ = require('lodash/core'),
  8. xml2js = require('xml2js'),
  9. HttpApi = require('./http-api');
  10. /**
  11. * Class for SOAP endpoint of Salesforce
  12. *
  13. * @protected
  14. * @class
  15. * @constructor
  16. * @param {Connection} conn - Connection instance
  17. * @param {Object} options - SOAP endpoint setting options
  18. * @param {String} options.endpointUrl - SOAP endpoint URL
  19. * @param {String} [options.xmlns] - XML namespace for method call (default is "urn:partner.soap.sforce.com")
  20. */
  21. var SOAP = module.exports = function(conn, options) {
  22. SOAP.super_.apply(this, arguments);
  23. this._endpointUrl = options.endpointUrl;
  24. this._xmlns = options.xmlns || 'urn:partner.soap.sforce.com';
  25. };
  26. inherits(SOAP, HttpApi);
  27. /**
  28. * Invoke SOAP call using method and arguments
  29. *
  30. * @param {String} method - Method name
  31. * @param {Object} args - Arguments for the method call
  32. * @param {Object} [schema] - Schema definition of response message
  33. * @param {Callback.<Object>} [callback] - Callback function
  34. * @returns {Promise.<Object>}
  35. */
  36. SOAP.prototype.invoke = function(method, args, schema, callback) {
  37. if (typeof schema === 'function') {
  38. callback = schema;
  39. schema = null;
  40. }
  41. var message = {};
  42. message[method] = args;
  43. return this.request({
  44. method: 'POST',
  45. url: this._endpointUrl,
  46. headers: {
  47. 'Content-Type': 'text/xml',
  48. 'SOAPAction': '""'
  49. },
  50. message: message
  51. }).then(function(res) {
  52. return schema ? convertType(res, schema) : res;
  53. }).thenCall(callback);
  54. };
  55. /* @private */
  56. function convertType(value, schema) {
  57. if (_.isArray(value)) {
  58. return value.map(function(v) {
  59. return convertType(v, schema && schema[0])
  60. });
  61. } else if (_.isObject(value)) {
  62. if (value.$ && value.$['xsi:nil'] === 'true') {
  63. return null;
  64. } else if (_.isArray(schema)) {
  65. return [ convertType(value, schema[0]) ];
  66. } else {
  67. var o = {};
  68. for (var key in value) {
  69. o[key] = convertType(value[key], schema && schema[key]);
  70. }
  71. return o;
  72. }
  73. } else {
  74. if (_.isArray(schema)) {
  75. return [ convertType(value, schema[0]) ];
  76. } else if (_.isObject(schema)) {
  77. return {};
  78. } else {
  79. switch(schema) {
  80. case 'string':
  81. return String(value);
  82. case 'number':
  83. return Number(value);
  84. case 'boolean':
  85. return value === 'true';
  86. default:
  87. return value;
  88. }
  89. }
  90. }
  91. }
  92. /** @override **/
  93. SOAP.prototype.beforeSend = function(request) {
  94. request.body = this._createEnvelope(request.message);
  95. };
  96. /** @override **/
  97. SOAP.prototype.isSessionExpired = function(response) {
  98. return response.statusCode === 500 &&
  99. /<faultcode>[a-zA-Z]+:INVALID_SESSION_ID<\/faultcode>/.test(response.body);
  100. };
  101. /** @override **/
  102. SOAP.prototype.parseError = function(body) {
  103. var error = lookupValue(body, [ /:Envelope$/, /:Body$/, /:Fault$/ ]);
  104. return {
  105. errorCode: error.faultcode,
  106. message: error.faultstring
  107. };
  108. };
  109. /** @override **/
  110. SOAP.prototype.getResponseBody = function(response) {
  111. var body = SOAP.super_.prototype.getResponseBody.call(this, response);
  112. return lookupValue(body, [ /:Envelope$/, /:Body$/, /.+/ ]);
  113. };
  114. /**
  115. * @private
  116. */
  117. function lookupValue(obj, propRegExps) {
  118. var regexp = propRegExps.shift();
  119. if (!regexp) {
  120. return obj;
  121. }
  122. else {
  123. for (var prop in obj) {
  124. if (regexp.test(prop)) {
  125. return lookupValue(obj[prop], propRegExps);
  126. }
  127. }
  128. return null;
  129. }
  130. }
  131. /**
  132. * @private
  133. */
  134. function toXML(name, value) {
  135. if (_.isObject(name)) {
  136. value = name;
  137. name = null;
  138. }
  139. if (_.isArray(value)) {
  140. return _.map(value, function(v) { return toXML(name, v); }).join('');
  141. } else {
  142. var attrs = [];
  143. var elems = [];
  144. if (_.isObject(value)) {
  145. for (var k in value) {
  146. var v = value[k];
  147. if (k[0] === '@') {
  148. k = k.substring(1);
  149. attrs.push(k + '="' + v + '"');
  150. } else {
  151. elems.push(toXML(k, v));
  152. }
  153. }
  154. value = elems.join('');
  155. } else {
  156. value = String(value)
  157. .replace(/&/g, '&amp;')
  158. .replace(/</g, '&lt;')
  159. .replace(/>/g, '&gt;')
  160. .replace(/"/g, '&quot;')
  161. .replace(/'/g, '&apos;');
  162. }
  163. var startTag = name ? '<' + name + (attrs.length > 0 ? ' ' + attrs.join(' ') : '') + '>' : '';
  164. var endTag = name ? '</' + name + '>' : '';
  165. return startTag + value + endTag;
  166. }
  167. }
  168. /**
  169. * @private
  170. */
  171. SOAP.prototype._createEnvelope = function(message) {
  172. var header = {};
  173. var conn = this._conn;
  174. if (conn.accessToken) {
  175. header.SessionHeader = { sessionId: this._conn.accessToken };
  176. }
  177. if (conn.callOptions) {
  178. header.CallOptions = conn.callOptions;
  179. }
  180. return [
  181. '<?xml version="1.0" encoding="UTF-8"?>',
  182. '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"',
  183. ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"',
  184. ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">',
  185. '<soapenv:Header xmlns="' + this._xmlns + '">',
  186. toXML(header),
  187. '</soapenv:Header>',
  188. '<soapenv:Body xmlns="' + this._xmlns + '">',
  189. toXML(message),
  190. '</soapenv:Body>',
  191. '</soapenv:Envelope>'
  192. ].join('');
  193. };