@twec/node-suite
Version:
Generic functionality for connecting to NetSuite Web Services from Node
164 lines (147 loc) • 5.61 kB
JavaScript
const got = require('got');
const fastxml = require('fast-xml-parser');
const xmlFormat = require('xml-formatter');
const debug = require('debug')('node-suite:soap');
const NetsuiteTBAuth = require('./auth');
const xml = require('./xml');
// "Standard Operations"
// https://system.sandbox.netsuite.com/app/help/helpcenter.nl?fid=section_N3478008.html
const OPERATIONS = {
get: { responseName: 'getResponse', innerResponseName: 'readResponse' },
search: { responseName: 'searchResponse', innerResponseName: 'searchResult' },
upsert: { responseName: 'upsertResponse', innerResponseName: 'writeResponse' },
// Add more as needed (add, update, etc.)
};
function handleError(err) {
const error = (typeof err === 'string') ? new Error(err) : err;
throw error;
}
function getNamespaces(version, body) {
const NAMESPACES = {
platformMsgs: `urn:messages_${version}.platform.webservices.netsuite.com`,
platformCore: `urn:core_${version}.platform.webservices.netsuite.com`,
platformFaults: `urn:faults_${version}.platform.webservices.netsuite.com`,
platformCommon: `urn:common_${version}.platform.webservices.netsuite.com`,
listRel: `urn:relationships_${version}.lists.webservices.netsuite.com`,
tranSales: `urn:sales_${version}.transactions.webservices.netsuite.com`,
actSched: `urn:scheduling_${version}.activities.webservices.netsuite.com`,
setupCustom: `urn:customization_${version}.setup.webservices.netsuite.com`,
listAcct: `urn:accounting_${version}.lists.webservices.netsuite.com`,
tranBank: `urn:bank_${version}.transactions.webservices.netsuite.com`,
tranCust: `urn:customers_${version}.transactions.webservices.netsuite.com`,
tranInvt: `urn:inventory_${version}.transactions.webservices.netsuite.com`,
listSupport: `urn:support_${version}.lists.webservices.netsuite.com`,
tranGeneral: `urn:general_${version}.transactions.webservices.netsuite.com`,
listMkt: `urn:marketing_${version}.lists.webservices.netsuite.com`,
listWebsite: `urn:website_${version}.lists.webservices.netsuite.com`,
fileCabinet: `urn:filecabinet_${version}.documents.webservices.netsuite.com`,
listEmp: `urn:employees_${version}.lists.webservices.netsuite.com`,
};
const namespaceAttributes = {};
const str = xml(body);
const nameSpaceKeys = Object.keys(NAMESPACES);
nameSpaceKeys.forEach((nameSpaceKey) => {
if (new RegExp(`${nameSpaceKey}:`, 'ig').test(str)) {
namespaceAttributes[`xmlns:${nameSpaceKey}`] = NAMESPACES[nameSpaceKey];
}
});
return namespaceAttributes;
}
async function doXmlSoapRequest(soapEnvelope, requestData, operation) {
const { url, method } = requestData;
const xmlBody = `<?xml version="1.0" encoding="utf-8" ?>${xml(soapEnvelope)}`;
const requestOptions = {
method,
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: operation,
},
body: xmlBody,
};
debug('request:', xmlFormat(xmlBody));
let response;
try {
response = await got(url, requestOptions);
} catch (err) {
if (!err.response) {
throw err;
}
return err.response;
}
return response;
}
function parseSoapBody(responseBody) {
const parsed = fastxml.parse(responseBody, {
attributeNamePrefix: '@',
ignoreAttributes: false,
ignoreNameSpace: true,
});
if (parsed && !parsed.Envelope) {
console.error('Invalid SOAP response:');
console.error(JSON.stringify(parsed, null, 2));
}
const soapBody = parsed.Envelope.Body;
return soapBody;
}
function parseInnerResponse(soapBody, operation) {
const op = OPERATIONS[operation];
if (!op) {
handleError(`Operation invalid or unsupported: ${operation}`);
}
debug('operation', operation, op, 'soapBody', soapBody);
const opResponse = soapBody[op.responseName] || {};
const innerResponse = opResponse[op.innerResponseName] || {};
return innerResponse;
}
function handleSoapFault(soapBodyFault) {
const error = new Error(soapBodyFault.faultstring);
error.code = soapBodyFault.faultcode;
error.detail = soapBodyFault.detail;
throw error;
}
async function soap({ config, operation, body }) {
const tokenPassport = new NetsuiteTBAuth({
consumer: config.oauth.consumer,
realm: config.passport.account,
});
const { version } = config;
const requestData = {
url: `https://${config.domain}/services/NetSuitePort_${version}`,
method: 'POST',
};
const authData = tokenPassport.authorize(requestData, config.oauth.token);
const soapEnvelope = {
name: 'soap:Envelope',
attributes: {
'xmlns:soap': 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:tns': `urn:platform_${version}.webservices.netsuite.com`,
},
children: [
{
name: 'soap:Header',
children: [tokenPassport.toNode(authData)],
},
{
name: 'soap:Body',
children: [
{
name: `platformMsgs:${operation}`,
children: [body],
},
],
},
],
};
const nsAttributes = getNamespaces(version, soapEnvelope);
soapEnvelope.attributes = Object.assign(soapEnvelope.attributes, nsAttributes);
const response = await doXmlSoapRequest(soapEnvelope, requestData, operation);
debug('response status: %s', response.statusCode);
// debug('response body:\n%s', xmlFormat(response.body));
const soapBody = parseSoapBody(response.body);
if (soapBody.Fault) {
handleSoapFault(soapBody.Fault);
}
return parseInnerResponse(soapBody, operation);
}
module.exports = soap;