clang
Advanced tools
Comparing version 0.2.10 to 1.0.1
@@ -1,1 +0,1 @@ | ||
module.exports = require('./lib/clang'); | ||
module.exports = require('./lib/clang') |
1011
lib/clang.js
'use strict' | ||
var _ = require('lodash'); | ||
var soap = require('soap'); | ||
var async = require('async'); | ||
var mock = require('./mock'); | ||
const soap = require('soap') | ||
const async = require('async') | ||
const path = require('path') | ||
const moment = require('moment'); | ||
const prompt = require('prompt'); | ||
const mock = require('./mock') | ||
const fields = require('./fields'); | ||
function Clang(config) { | ||
var me = this; | ||
class Clang { | ||
constructor(config) { | ||
const me = this | ||
if (!(me instanceof Clang)) { | ||
return new Clang(config); | ||
} | ||
me.config = _.clone(config || {}); | ||
me.config.normalizeOptionFields = me.config.normalizeOptionFields == undefined ? true : me.config.normalizeOptionFields | ||
me.config = Object.assign({}, config) | ||
me.wsdl = 'https://secure.myclang.com/app/api/soap/public/index.php?wsdl'; | ||
me.wsdl = 'https://secure.myclang.com/app/api/soap/public/index.php?wsdl' | ||
if (!me.config.version) { | ||
//By default this version is locked, because I now it works | ||
me.wsdl += '&version=1.23'; | ||
} else if (me.config.version !== '*') { | ||
//Get a specific version by supplying the version number | ||
me.wsdl += '&version=' + me.config.version; | ||
} | ||
me.api = null; | ||
if (!me.config.version) { | ||
//By default this version is locked, because I now it works | ||
me.wsdl += '&version=1.23' | ||
} else if (me.config.version !== '*') { | ||
//Get a specific version by supplying the version number | ||
me.wsdl += '&version=' + me.config.version | ||
} | ||
me.api = null | ||
if (me.config.mock) { | ||
mock(me) | ||
if (me.config.mock) { | ||
me.wsdl = path.join(__dirname, 'wsdl/version-1.24.xml') | ||
} | ||
} | ||
} | ||
Clang.prototype.request = function(methodName, args, callback) { | ||
var me = this; | ||
var requestArgs, pagingArgs = {}; | ||
request(/*methodName, args(optional), cb(optional)*/) { | ||
const me = this | ||
let requestArgs, normalizedArgs, pagingArgs = {} | ||
let methodName, cb | ||
let promiseMode | ||
if (arguments.length === 2) { | ||
callback = args; | ||
requestArgs = { | ||
uuid: me.config.uuid | ||
}; | ||
} else if (arguments.length === 3) { | ||
requestArgs = _.assign({ | ||
uuid: me.config.uuid | ||
}, args || {}); | ||
} else { | ||
callback = methodName; | ||
return setImmediate(function() { | ||
callback(new Error('request takes 3 arguments: methodName, args (object) and callback function')); | ||
}); | ||
} | ||
Array.prototype.slice.call(arguments).forEach(arg => { | ||
if (typeof arg === 'string') | ||
methodName = arg | ||
else if (arg && typeof arg === 'object') | ||
requestArgs = arg | ||
else if (typeof arg === 'function') | ||
cb = arg | ||
}) | ||
if (!cb) { | ||
promiseMode = true | ||
cb = function() {} | ||
} | ||
if (!requestArgs.uuid) { | ||
return setImmediate(function() { | ||
callback(new Error('uuid missing. Provide it with the 2nd arguments object or with client instantiation. Hint: `var clang = new Clang({uuid: \'12345678-...\'})`')); | ||
}); | ||
} | ||
return new Promise((resolve, reject) => { | ||
pagingArgs.offset = requestArgs._offset || 0; | ||
pagingArgs.size = requestArgs._size || 50; | ||
delete requestArgs._offset; | ||
delete requestArgs._size; | ||
const errAway = (error) => { | ||
if (promiseMode) { | ||
reject(error) | ||
} | ||
setImmediate(cb.bind(null, error)) | ||
} | ||
async.waterfall([ | ||
function async_init(callback){ | ||
if (me.api) { | ||
console.log('SOAP client already created'); | ||
return setImmediate(callback); | ||
requestArgs = requestArgs || {} | ||
requestArgs.uuid = requestArgs.uuid || me.config.uuid | ||
if (!methodName) { | ||
return errAway(`methodName argument (string) missing`) | ||
} | ||
if (me.config.debug) { | ||
console.log('Creating SOAP client'); | ||
if (!requestArgs.uuid && !me.config.mock) { | ||
return errAway(`uuid missing. Provide it with the 2nd arguments object or with client instantiation. Hint: 'let clang = new Clang({uuid: '12345678', ...})'`) | ||
} | ||
soap.createClient(me.wsdl, function(err, result) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
pagingArgs.offset = requestArgs._offset || 0 | ||
pagingArgs.size = requestArgs._size || 50 | ||
delete requestArgs._offset | ||
delete requestArgs._size | ||
if (me.config.debug) { | ||
console.log('SOAP client created'); | ||
} | ||
me.api = result; | ||
me.description = result.describe(); | ||
async.autoInject({ | ||
init: (callback) => { | ||
if (me.api) { | ||
if (me.config.debug) { | ||
console.log(`SOAP client already created`) | ||
} | ||
return setImmediate(callback) | ||
} | ||
if (me.config.debug) { | ||
console.log(`Creating SOAP client...`) | ||
} | ||
callback(); | ||
}); | ||
}, | ||
function async_request(callback){ | ||
var fn = me.api[methodName]; | ||
soap.createClient(me.wsdl, (err, result) => { | ||
if (err) { | ||
return callback(err) | ||
} | ||
if (!fn) { | ||
return setImmediate(function() { | ||
callback(new Error('Undefined method `' + methodName + '`')); | ||
}); | ||
} | ||
if (me.config.debug) { | ||
console.log(`SOAP client created`) | ||
} | ||
me.api = result | ||
me.description = result.describe() | ||
var args; | ||
callback() | ||
}) | ||
}, | ||
request: (init, cb) => { | ||
let fn = me.config.mock ? mock.api(me, methodName) : me.api[methodName] | ||
if (me.config.normalizeOptionFields) { | ||
args = normalizeInput(me.config, me.description, methodName, requestArgs); | ||
} else { | ||
args = requestArgs; | ||
} | ||
if (!fn) { | ||
return setImmediate(cb.bind(null, `Undefined method '${methodName}'`)) | ||
} | ||
fn(args, buildCallback(requestArgs.uuid, me.config, me.api, pagingArgs, callback)); | ||
} | ||
], function(err, result) { | ||
if (err) { | ||
if (err.root && err.root.Envelope && err.root.Envelope.Body) { | ||
/** | ||
* err.root.Envelope.Body: | ||
* { | ||
* "Fault": { | ||
* "faultcode": "105", | ||
* "faultstring": "Size must be between 1 and 50" | ||
* } | ||
* } | ||
*/ | ||
return callback(err.root.Envelope.Body); | ||
} | ||
return callback(err); | ||
} | ||
return callback(null, result); | ||
}); | ||
}; | ||
Clang.prototype.send = function(customer, options, callback) { | ||
var me = this; | ||
if (me.config.normalizeOptionFields) { | ||
normalizedArgs = me.normalizeInput(methodName, requestArgs) | ||
} else { | ||
normalizedArgs = requestArgs | ||
} | ||
var customerGetMethodName; | ||
var mergeMethodName; | ||
var sendMethodName; | ||
if (me.config.debug) { | ||
console.log(`Making SOAP request to ${methodName}...`) | ||
} | ||
if (arguments.length !== 3) { | ||
callback = customer; | ||
return setImmediate(function() { | ||
callback(new Error('send takes 3 arguments: customer, options and callback function')); | ||
}); | ||
fn(normalizedArgs, cb) | ||
}, | ||
output: ['request', (result, cb) => { | ||
// result is an array with [json result, raw xml response, soapHeader ] | ||
let [obj, raw, ] = result | ||
if (me.config.logPayload) { | ||
console.log('payload outgoing', me.api.lastRequest) | ||
console.log('payload incoming', raw) | ||
} | ||
if (me.config.debug) { | ||
console.log(`Incoming SOAP response ${JSON.stringify(obj).slice(0,50)}...`) | ||
} | ||
//Check for No SOAP Fault, but result code not 0 | ||
if (obj.code != 0) { | ||
return setImmediate(cb.bind(null, new Error(`No SOAP Fault, but result code not 0: ${JSON.stringify(obj.msg)}`))) | ||
} | ||
//Check for No SOAP Fault, but obj.msg = false (probably delete that didn't work out) | ||
if (obj.msg === false) { | ||
return setImmediate(cb.bind(null, new Error(`No SOAP Fault, but result msg is false (delete failed?): ${JSON.stringify(obj.msg)}`))) | ||
} | ||
if (obj.msg === null) { | ||
// this occurs for example when doing a customer_getByExternalId with a externalId that is not found | ||
if (me.config.debug) { | ||
console.log(`No records found in SOAP response (msg=null)`) | ||
} | ||
return setImmediate(cb.bind(null, null, [])) | ||
} | ||
if (obj.msg === undefined) { | ||
return setImmediate(cb.bind(null, new Error(`No SOAP Fault, but result msg is undefined (unexpected): ${JSON.stringify(obj.msg)}`))) | ||
} | ||
// result is id of resource of which the result is not there yet | ||
if (typeof obj.msg === 'string' && obj.msg.match(/^\d+$/)) { | ||
return getResourceSet(me, obj.msg, methodName, normalizedArgs, pagingArgs, cb) | ||
} | ||
return setImmediate(cb.bind(null, null, me.normalizeOutput(methodName, obj))) | ||
}] | ||
}, (err, result) => { | ||
// result is an object with the keys of `init` and `request` (as per autoInject) | ||
if (err) { | ||
let faultBody = err.root && err.root.Envelope && err.root.Envelope.Body | ||
if (faultBody) { | ||
/** | ||
* err.root.Envelope.Body: | ||
* { | ||
* "Fault": { | ||
* "faultcode": "105", | ||
* "faultstring": "Size must be between 1 and 50" | ||
* } | ||
* } | ||
*/ | ||
if ( | ||
faultBody.Fault.faultcode == 107 || | ||
faultBody.Fault.faultstring.match(/customer already member of/) | ||
) { | ||
console.log(`Incoming SOAP Fault ${JSON.stringify(faultBody)}, reduced to warning`) | ||
return cb(null, [{msg: true, warning: err.body}]) | ||
} | ||
console.log(`Incoming SOAP Fault ${JSON.stringify(faultBody)}`) | ||
return errAway(faultBody) | ||
} | ||
return errAway(err) | ||
} | ||
if (promiseMode) { | ||
return resolve(result.output) | ||
} | ||
cb(null, result.output) | ||
}) | ||
}) | ||
} | ||
if (!customer || typeof customer !== 'object' || !Object.keys(customer).length) { | ||
return setImmediate(function() { | ||
callback(new Error('The customer argument must be an non-empty object')); | ||
}); | ||
} | ||
options.lookup = options.lookup || 'externalId'; | ||
send(customer, options, callback) { | ||
const me = this | ||
customer = me.transformFields(customer, options) | ||
let customerGetMethodName | ||
let sendMethodName | ||
let lookupOption | ||
var customerArgs = {}; | ||
if (options.lookup === 'externalId') { | ||
customerGetMethodName = 'customer_getByExternalId'; | ||
customerArgs.externalId = customer.externalId; | ||
if (!customerArgs.externalId) { | ||
return setImmediate(function() { | ||
callback(new Error('customer.externalId is required with lookup method `' + options.lookup + '`')); | ||
}); | ||
if (arguments.length !== 3) { | ||
callback = customer | ||
return setImmediate(callback.bind(null, new Error(`send takes 3 arguments: customer, options and callback function`))) | ||
} | ||
} | ||
else if (options.lookup === 'customerId') { | ||
customerGetMethodName = 'customer_getById'; | ||
customerArgs.customerId = customer.id || customer.customerId; | ||
if (!customerArgs.customerId) { | ||
return setImmediate(function() { | ||
callback(new Error('customer.id/customerId is required with lookup method `' + options.lookup + '`')); | ||
}); | ||
if (!customer || typeof customer !== 'object' || !Object.keys(customer).length) { | ||
return setImmediate(callback.bind(null, new Error(`The customer argument must be an non-empty object`))) | ||
} | ||
} | ||
else if (options.lookup === 'email') { | ||
customerGetMethodName = 'customer_getByEmailAddress'; | ||
customerArgs.emailAddress = customer.email || customer.emailAddress; | ||
if (!customerArgs.emailAddress) { | ||
return setImmediate(function() { | ||
callback(new Error('customer.email/emailAddress is required with lookup method `' + options.lookup + '`')); | ||
}); | ||
lookupOption = (options.lookup || 'externalId').toLowerCase() | ||
try { | ||
// errors may be thrown while transforming | ||
customer = me.transformFields(customer, options) | ||
} catch (e) { | ||
return setImmediate(callback.bind(null, e)) | ||
} | ||
} | ||
else { | ||
return setImmediate(function() { | ||
callback(new Error('lookup method `' + options.lookup + '` is not supported')); | ||
}); | ||
} | ||
if (options.context && !options.contextId) { | ||
return setImmediate(function() { | ||
callback(new Error('When a send context is used, a contextId is mandatory')); | ||
}); | ||
} | ||
let customerArgs = {} | ||
if (lookupOption === 'externalid') { | ||
customerGetMethodName = 'customer_getByExternalId' | ||
customerArgs.externalId = customer.externalId | ||
if (!customerArgs.externalId) { | ||
return setImmediate(callback.bind(null, new Error(`customer.externalId is required with lookup method '${lookupOption}'`))) | ||
} | ||
} | ||
else if (lookupOption === 'customerid') { | ||
customerGetMethodName = 'customer_getById' | ||
customerArgs.customerId = customer.id || customer.customerId | ||
if (!customerArgs.customerId) { | ||
return setImmediate(callback.bind(null, new Error(`customer.id/customerId is required with lookup method '${lookupOption}'`))) | ||
} | ||
} | ||
else if (lookupOption === 'email' || lookupOption === 'emailaddress') { | ||
customerGetMethodName = 'customer_getByEmailAddress' | ||
customerArgs.emailAddress = customer.email || customer.emailAddress | ||
if (!customerArgs.emailAddress) { | ||
return setImmediate(callback.bind(null, new Error(`customer.email/emailAddress is required with lookup method '${lookupOption}'`))) | ||
} | ||
} | ||
else { | ||
return setImmediate(callback.bind(null, new Error(`lookup option '${lookupOption}' is not supported`))) | ||
} | ||
// only support send to group for now. Needs a backing Clang campaign listening to the group | ||
if (options.context && ['group'].indexOf(options.context) === -1 ) { | ||
return setImmediate(function() { | ||
callback(new Error('send to context `' + options.context + '` is not supported')); | ||
}); | ||
} | ||
if (options.context && !options.contextId) { | ||
return setImmediate(callback.bind(null, new Error(`When a send context is used, a contextId is mandatory`))) | ||
} | ||
async.waterfall([ | ||
function asyncWtfGet(callback) { | ||
me.request(customerGetMethodName, customerArgs, function(err, result) { | ||
if ( | ||
// Geen record gevonden zouden we via customer_getById als volgt weten | ||
err && err.Fault && (err.Fault.faultcode == 213 || err.Fault.faultstring.match(/not found/i)) || | ||
// via customer_getByExternalId als volgt | ||
result && !result.length | ||
) { | ||
if (!options.create) { | ||
return callback(new Error('Customer nog found and create is not allowed')); | ||
// only support send to group for now. Needs a backing Clang campaign listening to the group | ||
if (options.context && ['group', 'email'].indexOf(options.context) === -1 ) { | ||
return setImmediate(callback.bind(null, new Error(`send to context '${options.context}' is not supported`))) | ||
} | ||
async.autoInject({ | ||
get: (cb) => { | ||
me.request(customerGetMethodName, customerArgs, (err, result) => { | ||
let isArray = result instanceof Array | ||
let numRecords | ||
if ( | ||
err && | ||
err.Fault && | ||
( | ||
err.Fault.faultcode == 213 || | ||
err.Fault.faultstring.match(/not found/i) | ||
) | ||
) { | ||
// No record found with customer_getById | ||
numRecords = 0 | ||
} | ||
else if (err) { | ||
return cb(err) | ||
} | ||
else { | ||
numRecords = isArray ? result.length : result ? 1 : 0 | ||
} | ||
if (numRecords === 0 && !options.create) { | ||
return cb(new Error(`Customer not found and create is not allowed`)) | ||
} | ||
if (numRecords > 1) { | ||
return cb(new Error(`Multiple customers returned after get`)) | ||
} | ||
cb(null, isArray ? result[0] : result) | ||
}) | ||
}, | ||
upsertMethodName: (get, cb) => { | ||
setImmediate(cb.bind(null, null, get ? 'customer_update' : 'customer_insert')) | ||
}, | ||
upsert: (get, upsertMethodName, cb) => { | ||
if (get) { | ||
// The found record should be included in the supplied data | ||
// to make sure that record is updated. | ||
customer.id = get.id | ||
} | ||
else if (err) { | ||
return callback(err); | ||
} | ||
if (result && result.length > 1) { | ||
return callback(new Error('Multiple customers returned after get')); | ||
} | ||
callback(null, result.length ? result[0] : null); | ||
}); | ||
}, | ||
function asyncWtfMerge(record, callback) { | ||
if (record) { | ||
// The found record should be included in the supplied data | ||
// to make sure that record is updated. | ||
customer.id = record.id; | ||
mergeMethodName = 'customer_update'; | ||
} else { | ||
mergeMethodName = 'customer_insert'; | ||
} | ||
me.request(mergeMethodName, {customer: customer}, function(err, result) { | ||
if (err) { | ||
return callback(err); | ||
me.request(upsertMethodName, {customer}, cb) | ||
}, | ||
send: ['upsert', (upsert, cb) => { | ||
let args = {} | ||
if (options.context === 'group') { | ||
sendMethodName = 'group_addMember' | ||
args.group = { | ||
id: options.contextId | ||
} | ||
args.customer = { | ||
id: upsert.id | ||
} | ||
} | ||
if (!result || !result.length) { | ||
return callback(new Error('No customer returned after ' + mergeMethodName)); | ||
else if (options.context === 'email') { | ||
sendMethodName = 'email_sendToCustomer' | ||
args = Object.assign({}, customer, { | ||
emailId: options.contextId, | ||
customerId: upsert.id | ||
}) | ||
} | ||
if (result.length > 1) { | ||
return callback(new Error('Multiple customers returned after ' + mergeMethodName)); | ||
if (sendMethodName) { | ||
return me.request(sendMethodName, args, cb) | ||
} | ||
callback(null, result[0]); | ||
}); | ||
}, | ||
function asyncWtfSend(record, callback) { | ||
var args = {}; | ||
if (options.context === 'group') { | ||
sendMethodName = 'group_addMember'; | ||
args.group = { | ||
id: options.contextId | ||
}; | ||
args.customer = { | ||
id: record.id | ||
}; | ||
console.warn('No context found, just synced customer', JSON.stringify(customerArgs), ' with Clang') | ||
setImmediate(cb) | ||
}] | ||
}, callback) | ||
// result to final callback is an object with the keys of | ||
// `get`, `upsertMethodName`, `upsert` and `send` (as per autoInject) | ||
} | ||
transformFields(customer, options) { | ||
const me = this | ||
options = options || {} | ||
options.fieldMap = Object.assign({ | ||
// default field mappings | ||
particle: 'prefix', | ||
email: 'emailAddress', | ||
gender: { | ||
fieldName: 'gender', | ||
values: { | ||
M: 'MAN', | ||
F: 'WOMAN' | ||
} | ||
} | ||
else if (options.context === 'email') { | ||
// UNTESTED !!!!!!!!!!!!!!!!!!!!!!!!!! | ||
sendMethodName = 'email_sendToCustomer'; | ||
args = _.clone(customer); | ||
args.emailId = options.contextId; | ||
} | ||
}, options.fieldMap) | ||
if (sendMethodName) { | ||
return me.request(sendMethodName, args, callback); | ||
// create a new object with altered keys and values based on the fieldMap configuration | ||
let transformedCustomer = {} | ||
Object.keys(customer).forEach((key) => { | ||
let value = customer[key] | ||
let fieldMapTo = options.fieldMap[key] | ||
if (typeof fieldMapTo === 'string') { | ||
// set the value on the new key | ||
transformedCustomer[fieldMapTo] = me.safeValue('customer', fieldMapTo, value) | ||
} | ||
else if (typeof fieldMapTo === 'object') { | ||
// take the fieldName and values configuration of this fieldMap to transform the key and value | ||
transformedCustomer[fieldMapTo.fieldName] = me.safeValue('customer', fieldMapTo.fieldName, fieldMapTo.values[value] || value) | ||
} | ||
else { | ||
// set the value on the same key | ||
transformedCustomer[key] = me.safeValue('customer', key, value) | ||
} | ||
}) | ||
return transformedCustomer | ||
} | ||
console.warn('No context found, just synced customer', JSON.stringify(customerArgs), ' with Clang'); | ||
setImmediate(function() { | ||
callback(null, record); | ||
}); | ||
safeValue(resourceName, fieldName, value) { | ||
if (isObject(value) || value instanceof Array) { | ||
value = JSON.stringify(value) | ||
} | ||
], callback); | ||
}; | ||
Clang.prototype.transformFields = function(customer, options) { | ||
options = options || {} | ||
options.fieldMap = Object.assign({ | ||
// default field mappings | ||
particle: 'prefix', | ||
email: 'emailAddress', | ||
gender: { | ||
fieldName: 'gender', | ||
values: { | ||
M: 'MAN', | ||
F: 'WOMAN' | ||
let fieldDefinition = fields[resourceName] && fields[resourceName][fieldName] | ||
if (!fieldDefinition) { | ||
if (value instanceof Date) { | ||
return moment(value).format('YYYY-MM-DD HH:mm:ss') | ||
} | ||
return value | ||
} | ||
}, options.fieldMap || {}) | ||
// create a new object with altered keys and values based on the fieldMap configuration | ||
let transformedCustomer = {} | ||
Object.keys(customer).forEach((key) => { | ||
let value = customer[key] | ||
let fieldMapTo = options.fieldMap[key] | ||
if (typeof fieldMapTo === 'string') { | ||
// set the value on the new key | ||
transformedCustomer[fieldMapTo] = value | ||
if (fieldDefinition.type === 'date') { | ||
return moment(value).format('YYYY-MM-DD') | ||
} | ||
else if (typeof fieldMapTo === 'object') { | ||
// take the fieldName and values configuration of this fieldMap to transform the key and value | ||
transformedCustomer[fieldMapTo.fieldName] = fieldMapTo.values[value] || value | ||
if (fieldDefinition.type === 'datetime') { | ||
return moment(value).format('YYYY-MM-DD HH:mm:ss') | ||
} | ||
else { | ||
// set the value on the same key | ||
transformedCustomer[key] = value | ||
if (value && fieldDefinition.enum && fieldDefinition.enum.indexOf(value) === -1) { | ||
throw new Error(`Value of '${value}' is not one of the allowed values (${fieldDefinition.enum.join(',')}) of ${resourceName}.${fieldName}`) | ||
} | ||
}) | ||
return transformedCustomer | ||
}; | ||
if (value && fieldDefinition.maxlength && (value.length > fieldDefinition.maxlength)) { | ||
throw new Error(`Length of '${value}' (${value.length}) is longer than the allowed maxlength (${fieldDefinition.maxlength}) of ${resourceName}.${fieldName}`) | ||
} | ||
//Private stuff starts here | ||
var setMethods = { | ||
CUSTOMER_SET: 'customerSet_getCustomers', | ||
MAILING_SET : 'mailingSet_getMailings', | ||
OPEN_SET : 'openSet_getOpens', | ||
CLICK_SET : 'clickSet_getClicks', | ||
BOUNCE_SET : 'bounceSet_getBounces' | ||
}; | ||
return value | ||
} | ||
/** | ||
* Move arguments to an options object | ||
*/ | ||
var normalizeInput = function(config, description, clangMethodName, args) { | ||
/** | ||
* Move arguments to an options object | ||
*/ | ||
normalizeInput (clangMethodName, args) { | ||
const me = this | ||
var inputDefinition = description.clangAPI.clangPort[clangMethodName].input; | ||
let inputDefinition = me.description.clangAPI.clangPort[clangMethodName].input | ||
//To understand this code better, check how the entire interface definition | ||
// looks like in JSON created by createClient in wsdl 1.18.json | ||
//To understand this code better, check how the entire interface definition | ||
// looks like in JSON created by createClient in wsdl/version-1.24.json | ||
var optionsIdentifier, optionIdentifier; | ||
let optionsIdentifier, optionIdentifier | ||
var clangObjectName; | ||
if (clangMethodName.match('^email_sendTo')) { | ||
optionsIdentifier = 'manualOptions'; | ||
optionIdentifier = 'Option'; | ||
clangObjectName = 'email'; | ||
} else if (clangMethodName.match('^customer')) { | ||
optionsIdentifier = 'options'; | ||
optionIdentifier = 'CustomerOption'; | ||
clangObjectName = 'customer'; | ||
} else { | ||
return args; | ||
} | ||
let clangObjectName | ||
if (clangMethodName.match('^email_sendTo')) { | ||
optionsIdentifier = 'manualOptions' | ||
optionIdentifier = 'Option' | ||
clangObjectName = 'email' | ||
} else if (clangMethodName.match('^customer')) { | ||
optionsIdentifier = 'options' | ||
optionIdentifier = 'CustomerOption' | ||
clangObjectName = 'customer' | ||
} else { | ||
return args | ||
} | ||
var normalizedArgs = _.clone(args); | ||
let normalizedArgs = Object.assign({}, args) | ||
var moveOptions = function (object, inputDefinition) { | ||
Object.keys(object).forEach(function(key) { | ||
if (typeof inputDefinition[key] !== 'string') { | ||
if (!object[optionsIdentifier]) { | ||
object[optionsIdentifier] = {}; | ||
object[optionsIdentifier][optionIdentifier] = []; | ||
const moveOptions = (object, inputDefinition) => { | ||
Object.keys(object).forEach((key) => { | ||
if (typeof inputDefinition[key] !== 'string') { | ||
if (!object[optionsIdentifier]) { | ||
object[optionsIdentifier] = {} | ||
object[optionsIdentifier][optionIdentifier] = [] | ||
} | ||
object[optionsIdentifier][optionIdentifier].push({ | ||
name : key, | ||
value: object[key] | ||
}) | ||
delete object[key] | ||
} | ||
object[optionsIdentifier][optionIdentifier].push({ | ||
name : key, | ||
value: object[key] | ||
}); | ||
delete object[key]; | ||
} | ||
}); | ||
}; | ||
}) | ||
} | ||
if (args[clangObjectName] && Object.prototype.toString.call(args[clangObjectName]) === '[object Object]') { | ||
moveOptions(normalizedArgs[clangObjectName], inputDefinition[clangObjectName]); | ||
} else { | ||
moveOptions(normalizedArgs, inputDefinition); | ||
if (args[clangObjectName] && Object.prototype.toString.call(args[clangObjectName]) === '[object Object]') { | ||
moveOptions(normalizedArgs[clangObjectName], inputDefinition[clangObjectName]) | ||
} else { | ||
moveOptions(normalizedArgs, inputDefinition) | ||
} | ||
return normalizedArgs | ||
} | ||
return normalizedArgs; | ||
}; | ||
/** | ||
* Basically the result is always {code: [int], msg: [object]} | ||
* Some methods return an immediate object as the msg. | ||
* For example: group_getById outputs: | ||
* { | ||
* code: 0, | ||
* msg: { id: '60', parentId: '0', name: 'test', description: '', ... } | ||
* } | ||
* | ||
* But group_getByObject outputs: | ||
* { | ||
* code: 0, | ||
* msg: { | ||
* Group: [ | ||
* { id: '60', parentId: '0', name: 'test', description: ', ... }, | ||
* ... | ||
* ] | ||
* } | ||
* } | ||
* And some methods like customer_delete output: | ||
* { | ||
* code: 0, | ||
* msg: true | ||
* } | ||
*/ | ||
normalizeOutput (clangMethodName, result) { | ||
const me = this | ||
const outputDefinition = me.description.clangAPI.clangPort[clangMethodName].output | ||
/** | ||
* Normalizes response to an array of objects | ||
*/ | ||
var normalizeResponse = function(config, result) { | ||
if (result.msg === true) { | ||
if (config.debug) { | ||
console.log('SOAP success'); | ||
} | ||
return [{msg: true}]; | ||
} | ||
outputDefinition.outputType = outputDefinition.outputType || (() => { | ||
let outputKeys, match | ||
if (typeof outputDefinition.msg === 'string') {// like "xsd:boolean" | ||
return 'primitive' | ||
} | ||
var msgKeys = Object.keys(result.msg); | ||
if (msgKeys.length === 0) { | ||
if (config.debug) { | ||
console.log('No records found in SOAP response'); | ||
outputKeys = Object.keys(outputDefinition.msg) | ||
.filter(key=>( | ||
key !== 'targetNSAlias' && | ||
key !== 'targetNamespace' | ||
)) | ||
if (outputKeys.length === 1 && (match = outputKeys[0].match(/(.*)\[\]$/))) { | ||
// Matches Group[] | ||
return match[1] | ||
} | ||
return 'object' | ||
})() | ||
if (outputDefinition.outputType === 'primitive') { | ||
return result.msg | ||
} | ||
return []; | ||
} | ||
if (msgKeys.length === 1 && result.msg[msgKeys[0]] instanceof Array) { | ||
if (config.debug) { | ||
console.log('SOAP msg array returned'); //{ code: 0, msg: { Group: [Object] } } | ||
if (outputDefinition.outputType === 'object') { | ||
return result.msg | ||
} | ||
return result.msg[msgKeys[0]]; | ||
return result.msg[outputDefinition.outputType] | ||
} | ||
if (result.msg) { // 1 record from x_getById or x_create | ||
if (config.debug) { | ||
console.log('SOAP msg object returned'); | ||
} | ||
return [result.msg]; | ||
} | ||
}; | ||
/** | ||
* Factory for clang method callback builder. It deals with: | ||
* - getting a resource id status | ||
* - getting the data set when the resource is READY | ||
* - normalizing the response | ||
* @param config - a reference to the configuration passed when instantiating Clang | ||
* @param api - a reference to a previously created soap client with Clang's wsdl and node-soap | ||
* @param callback - the method for handling the final data set | ||
*/ | ||
var buildCallback = function (uuid, config, api, pagingArgs, callback) { | ||
deleteAll(resourceName, cb) { | ||
const me = this | ||
return function apiCallback(err, result) { | ||
if (config.logRequests) { | ||
console.log(api.lastRequest); | ||
} | ||
/* | ||
first result that indicate there's a recource looks like this: { code: 0, msg: '210577' } | ||
second result of fetching resource status looks like this result looks like: { | ||
code: 0, | ||
msg: { | ||
id : "147082", | ||
type : "CUSTOMER_SET", | ||
status: "READY", | ||
size : "1" | ||
async.autoInject({ | ||
getAll: (cb) => { | ||
me.request(`${resourceName}_getAll`, cb) | ||
}, | ||
prompt: (getAll, cb) => { | ||
if (me.config.mock) { | ||
return setImmediate(cb.bind(null, null, {answer: 'y'})) | ||
} | ||
prompt.start() | ||
prompt.get({ | ||
name: 'answer', | ||
message: `Sure you want to delete all ${getAll.length} ${resourceName}s`, | ||
validator: /y[es]*|n[o]?/i, | ||
warning: 'Must respond [Y]es or [N]o', | ||
required: false, | ||
default: 'No' | ||
}, cb) | ||
}, | ||
deleteAll: (getAll, prompt, cb) => { | ||
console.log('prompt', prompt) | ||
if (!prompt.answer.match(/y[es]*/i)) { | ||
return setImmediate(cb) | ||
} | ||
console.log(`Deleting ${getAll.length} ${resourceName}s...`) | ||
async.map(getAll, (item, cb) => { | ||
if (resourceName === 'customer') { | ||
me.request(`${resourceName}_delete`, {[resourceName]: item}, cb) | ||
} else { | ||
me.request(`${resourceName}_delete`, {[resourceName+'Id']: item.id}, cb) | ||
} | ||
}, cb) | ||
} | ||
*/ | ||
var resourceId; | ||
if (err) { | ||
console.log('SOAP error arrived', err.body); | ||
} | ||
}, cb) | ||
} | ||
} | ||
//Check for actual SOAP Fault generated by callback in soap/lib/client line 152 | ||
if (err) { | ||
if (err.body && | ||
err.body.match(/<faultcode>107<\/faultcode>/) && | ||
err.body.match(/customer already member of/) ) | ||
return callback(null, [{msg: true, warning: err.body}]); | ||
return callback(err); | ||
} | ||
//Check for No SOAP Fault, but result code not 0 | ||
if (result.code != 0) { | ||
return callback(new Error('No SOAP Fault, but result code not 0: ' + JSON.stringify(result.msg))); | ||
} | ||
//Check for No SOAP Fault, but result.msg = false (probably delete that didn't work out) | ||
if (result.msg === false) { | ||
return callback(new Error('No SOAP Fault, but result msg is false (delete failed?): ' + JSON.stringify(result.msg))); | ||
} | ||
if (result.msg === null) { | ||
// this occurs for example when doing a customer_getByExternalId with a externalId that is not found | ||
//Private stuff starts here | ||
const isObject = o => (!!o) && (o.constructor === Object) | ||
const getResourceSet = (instance, resourceId, methodName, normalizedArgs, pagingArgs, cb) => { | ||
const setMethods = { | ||
CUSTOMER_SET: 'customerSet_getCustomers', | ||
MAILING_SET : 'mailingSet_getMailings', | ||
OPEN_SET : 'openSet_getOpens', | ||
CLICK_SET : 'clickSet_getClicks', | ||
BOUNCE_SET : 'bounceSet_getBounces' | ||
} | ||
let config = instance.config | ||
let api = instance.api | ||
let uuid = normalizedArgs.uuid | ||
async.autoInject({ | ||
poll: (cb) => { | ||
async.doWhilst(cb => { | ||
if (config.debug) { | ||
console.log(`Checking state of resource: ${resourceId} for ${methodName}`) | ||
} | ||
api.resource_getById({ uuid, resourceId }, (err, result) => { | ||
if (err) return cb(err) | ||
if (config.debug) { | ||
console.log(`Resource ${result.msg.type} = ${result.msg.status} (id: ${result.msg.id}, records: ${result.msg.size})`) | ||
} | ||
if (result && result.msg.status === 'ERROR') { | ||
return cb(new Error(`Error getting resource ${resourceId} (probably incorrect x_getByObject request parameters)`)) | ||
} | ||
if (result && result.msg.status === 'CLOSED') { | ||
return cb(new Error(`Resource ${resourceId} was closed unexpectedly`)) | ||
} | ||
cb(null, result) | ||
}) | ||
}, (result) => (result.msg.status === 'PROCESSING'), cb) | ||
}, | ||
/** | ||
* @param {object} poll - result of READY resource set | ||
* { | ||
* code: 0, | ||
* msg: { | ||
* id : "147082", | ||
* type : "CUSTOMER_SET", | ||
* status: "READY", | ||
* size : "1" | ||
* } | ||
* } | ||
*/ | ||
getData: (poll, cb) => { | ||
if (config.debug) { | ||
console.log('No records found in SOAP response (msg=null)'); | ||
console.log(`Getting records from resourceId: ${resourceId} using ${setMethods[poll.msg.type]}`) | ||
} | ||
return callback(null, []); | ||
} | ||
if (result.msg === undefined) { | ||
return callback(new Error('No SOAP Fault, but result msg is undefined (unexpected): ' + JSON.stringify(result.msg))); | ||
} | ||
//When resource is READY fetch the data using the resourceMethod | ||
if (result.msg.status === 'ERROR') { | ||
return callback(new Error('Error getting resource (probably incorrect x_getByObject request parameters)')); | ||
} | ||
//Check for result that should be fetched using new SOAP call | ||
if (typeof result.msg === 'string' || //first attempt trying if resource is READY | ||
result.msg.status === 'PROCESSING' //next attempts trying if resource is READY | ||
) { | ||
resourceId = typeof result.msg === 'string' ? result.msg : result.msg.id; | ||
if (config.debug) { | ||
console.log('Invoking SOAP method to see if resourceId is READY: ' + resourceId); | ||
if (!(poll.msg.size > 0)) { | ||
if (config.debug) { | ||
console.log(`No records found in SOAP response`) | ||
} | ||
// poll.msg.size = 0, no need to go fetch result set | ||
return setImmediate(cb.bind(null, null, [])) | ||
} | ||
api.resource_getById({ | ||
uuid : uuid, | ||
resourceId: resourceId | ||
}, apiCallback); | ||
return; | ||
} | ||
if (result.msg.status === 'READY') { | ||
resourceId = result.msg.id; | ||
if (config.debug) { | ||
console.log('Resource of resourceId: ' + resourceId + ' is READY ('+result.msg.size+ ' records)'); | ||
} | ||
if (result.msg.size > 0) { | ||
async.series([ | ||
function(callback){ | ||
if (config.debug) { | ||
console.log('Getting data from resource with resourceId: ' + resourceId + ' using ' + setMethods[result.msg.type]); | ||
} | ||
api[setMethods[result.msg.type]]({ | ||
uuid : uuid, | ||
resourceId: resourceId, | ||
offset : pagingArgs.offset, | ||
size : pagingArgs.size | ||
}, function (err, result) { | ||
if (config.logRequests) { | ||
console.log(api.lastRequest); | ||
} | ||
if (err) { | ||
if (config.debug) { | ||
console.error('Error in getting resource set'); | ||
} | ||
return callback(err); | ||
} | ||
callback(null, normalizeResponse(config, result)); | ||
}); | ||
}, | ||
function(callback){ | ||
if (config.debug) { | ||
console.log('Free resource with resourceId: ' + resourceId); | ||
} | ||
api.resource_free({ | ||
uuid : uuid, | ||
resourceId: resourceId | ||
}, callback); | ||
let setMethodName = setMethods[poll.msg.type] | ||
api[setMethodName]({ | ||
uuid, resourceId, | ||
offset : pagingArgs.offset, | ||
size : pagingArgs.size | ||
}, (err, result) => { | ||
if (config.logPayload) { | ||
console.log('payload outgoing', api.lastRequest) | ||
// console.log('payload incoming', raw) | ||
} | ||
if (err) { | ||
if (config.debug) { | ||
console.error('Error in getting resource set') | ||
} | ||
], | ||
function(err, results) { | ||
if (config.logRequests) { | ||
console.log(api.lastRequest); | ||
} | ||
callback(err, results[0]); | ||
}); | ||
return; | ||
} | ||
return cb(err) | ||
} | ||
cb(null, instance.normalizeOutput(setMethodName, result)) | ||
}) | ||
}, | ||
freeResource: (getData, cb) => { | ||
if (config.debug) { | ||
console.log('No records found in SOAP response'); | ||
console.log(`Free resource with resourceId: ${resourceId}`) | ||
} | ||
return callback(null, []); | ||
api.resource_free({ uuid, resourceId }, cb) | ||
} | ||
if (result.msg) { | ||
return callback(null, normalizeResponse(config, result)); | ||
}, (err, result) => { | ||
if (config.logPayload) { | ||
console.log('payload outgoing', api.lastRequest) | ||
// console.log('payload incoming', raw) | ||
} | ||
cb(err, result.getData) | ||
}) | ||
} | ||
return callback(new Error('Unexpected and unhandled response: ' + JSON.stringify(result.msg))); | ||
}; | ||
}; | ||
module.exports = Clang; | ||
module.exports = Clang |
@@ -1,69 +0,88 @@ | ||
module.exports = [ | ||
{ | ||
methodName: 'customer_insert', | ||
result: [ | ||
{ | ||
smsOptIn: 'Y', | ||
optIn: 'Y', | ||
id: '14880', | ||
title: '', | ||
lastname: '', | ||
firstname: '', | ||
middlename: '', | ||
prefix: '', | ||
suffix: '', | ||
initials: '', | ||
gender: 'UNKNOWN', | ||
maritalStatus: 'UNKNOWN', | ||
birthday: '0000-00-00', | ||
birthplace: '', | ||
address: '', | ||
address2: '', | ||
address3: '', | ||
addressNumber: '', | ||
addressNumberSuffix: '', | ||
zipCode: '', | ||
poBox: '0', | ||
city: '', | ||
state: '', | ||
country: '', | ||
companyName: '', | ||
department: '', | ||
alternateAddress: '', | ||
alternateAddress2: '', | ||
alternateAddress3: '', | ||
alternateAddressNumber: '', | ||
alternateAddressNumberSuffix: '', | ||
alternateZipCode: '', | ||
alternateCity: '', | ||
alternateState: '', | ||
alternateCountry: '', | ||
contactMan: '', | ||
jobTitle: '', | ||
workPhone: '', | ||
workExtension: '', | ||
workMobile: '', | ||
homePhone: '', | ||
mobilePhone: '', | ||
faxNumber: '', | ||
emailAddress: '', | ||
alternateEmailAddress: '', | ||
emailType: 'MULTIPART', | ||
mailStatus: 'ACTIVE', | ||
softBounceCount: '0', | ||
hardBounceCount: '0', | ||
website: '', | ||
imageUrl: '', | ||
status: 'SUSPECT', | ||
externalId: '', | ||
userName: '', | ||
password: '', | ||
createdBy: 'SOAP user (int)', | ||
createdAt: '2017-06-23 18:01:57', | ||
modifiedBy: 'SOAP user (int)', | ||
modifiedAt: '2017-06-23 18:01:57', | ||
options: [] | ||
} | ||
] | ||
module.exports = { | ||
customer: { | ||
smsOptIn: 'Y', | ||
optIn: 'Y', | ||
id: '14880', | ||
title: '', | ||
lastname: '', | ||
firstname: '', | ||
middlename: '', | ||
prefix: '', | ||
suffix: '', | ||
initials: '', | ||
gender: 'UNKNOWN', | ||
maritalStatus: 'UNKNOWN', | ||
birthday: '0000-00-00', | ||
birthplace: '', | ||
address: '', | ||
address2: '', | ||
address3: '', | ||
addressNumber: '', | ||
addressNumberSuffix: '', | ||
zipCode: '', | ||
poBox: '0', | ||
city: '', | ||
state: '', | ||
country: '', | ||
companyName: '', | ||
department: '', | ||
alternateAddress: '', | ||
alternateAddress2: '', | ||
alternateAddress3: '', | ||
alternateAddressNumber: '', | ||
alternateAddressNumberSuffix: '', | ||
alternateZipCode: '', | ||
alternateCity: '', | ||
alternateState: '', | ||
alternateCountry: '', | ||
contactMan: '', | ||
jobTitle: '', | ||
workPhone: '', | ||
workExtension: '', | ||
workMobile: '', | ||
homePhone: '', | ||
mobilePhone: '', | ||
faxNumber: '', | ||
emailAddress: '', | ||
alternateEmailAddress: '', | ||
emailType: 'MULTIPART', | ||
mailStatus: 'ACTIVE', | ||
softBounceCount: '0', | ||
hardBounceCount: '0', | ||
website: '', | ||
imageUrl: '', | ||
status: 'SUSPECT', | ||
externalId: '', | ||
userName: '', | ||
password: '', | ||
createdBy: 'SOAP user (int)', | ||
createdAt: '2017-06-23 18:01:57', | ||
modifiedBy: 'SOAP user (int)', | ||
modifiedAt: '2017-06-23 18:01:57', | ||
options: [] | ||
}, | ||
email: { | ||
folder: '/', | ||
class: 'EMAIL', | ||
id: '59846', | ||
type: 'MULTIPART', | ||
templateId: '0', | ||
name: 'test-name', | ||
campaignId: '0', | ||
fromAddress: 'user@example.com', | ||
fromName: 'Smith', | ||
replyToAddress: '', | ||
replyToName: '', | ||
subject: 'test-name', | ||
createdBy: 'SOAP user (ext)', | ||
createdAt: '2017-07-05 14:34:41', | ||
modifiedBy: 'SOAP user (ext)', | ||
modifiedAt: '2017-07-05 14:34:41', | ||
htmlContent: '<html><body><h1>Newsletter</h1>HTML body</body></html>', | ||
htmlBlocks: null, | ||
textContent: 'Newsletter text body' | ||
}, | ||
empty_email: { | ||
type: 'MULTIPART' | ||
} | ||
] | ||
} |
154
lib/mock.js
@@ -5,14 +5,152 @@ 'use strict' | ||
module.exports = function(clang) { | ||
clang.request = function(methodName, data, cb) { | ||
let result = (fixtures.find(item => item.methodName === methodName) || { | ||
result: null | ||
}).result | ||
const database = { | ||
customer:[], | ||
email :[], | ||
group :[] | ||
} | ||
if (methodName === 'customer_insert') { | ||
result[0] = Object.assign({}, result[0], data.customer) | ||
const newId = (resourceName) => { | ||
const table = database[resourceName] | ||
if (!table) return 1 | ||
return 1 + table.reduce((acc, item) => Math.max(acc, item.id), 0) | ||
} | ||
module.exports.api = (clang, methodName) => { | ||
return (normalizedArgs, cb) => { | ||
const [,resourceName, resourceMethod] = methodName.match(/^(\w+)_(\w+)/) | ||
const table = database[resourceName] | ||
let items, index, stripped | ||
let nestedArgs = normalizedArgs[resourceName] | ||
if (resourceMethod === 'create') { | ||
items = Object.assign({}, fixtures['empty_' + resourceName] || fixtures[resourceName]) | ||
} | ||
if (resourceMethod === 'insert') { | ||
if (resourceName === 'email') { | ||
if (!nestedArgs) { | ||
return setImmediate(cb.bind(null, {Fault: {faultcode: 105}})) | ||
} | ||
if (!nestedArgs.type) { | ||
return setImmediate(cb.bind(null, {Fault: {faultcode: 105}})) | ||
} | ||
if (!nestedArgs.name) { | ||
return setImmediate(cb.bind(null, {Fault: {faultcode: 751}})) | ||
} | ||
} | ||
stripped = Object.assign({}, fixtures[resourceName], nestedArgs, {id: newId(resourceName)}) | ||
setImmediate(cb.bind(null, null, result)) | ||
table.push(stripped) | ||
items = table[table.length - 1] | ||
} | ||
if (resourceMethod === 'update') { | ||
items = table.find(item => item.id === (nestedArgs && nestedArgs.id)) | ||
if (items) { | ||
items = Object.assign({}, items, nestedArgs) | ||
} else { | ||
return setImmediate(cb.bind(null, {Fault: {faultcode: 213}})) | ||
} | ||
} | ||
if (resourceMethod === 'delete') { | ||
index = table.findIndex(item => { | ||
// fix inconsistency between xxx_delete methods | ||
if (resourceName === 'customer') { | ||
return item.id === (nestedArgs && nestedArgs.id) | ||
} | ||
return item.id === normalizedArgs[resourceName + 'Id'] | ||
}) | ||
if (index > -1) { | ||
table.splice(index, 1) | ||
} | ||
items = true | ||
} | ||
if (resourceMethod === 'getById') { | ||
items = table.find(item => item.id === normalizedArgs[resourceName + 'Id']) | ||
items = Object.assign({}, items) | ||
} | ||
if (resourceMethod === 'getByEmailAddress') { | ||
items = table.filter(item => item.id === normalizedArgs['emailAddress']) | ||
} | ||
if (resourceMethod === 'getByExternalId') { | ||
items = table.filter(item => item.id === normalizedArgs['externalId']) | ||
} | ||
if (resourceMethod === 'getByObject') { | ||
items = table.filter(item => !Object.keys(nestedArgs).find(key => nestedArgs[key] != item[key])) | ||
} | ||
if (resourceMethod === 'getAll') { | ||
items = table.filter(item => !!item) // make copy | ||
} | ||
const result = { | ||
code: 0, | ||
msg: getOutput( | ||
clang, | ||
methodName, | ||
items | ||
) | ||
} | ||
setImmediate(cb.bind(null, null, [result, 'fake-soap-reponse', 'soap-headers'])) | ||
} | ||
} | ||
/** | ||
* Basically the result is always {code: [int], msg: [object]} | ||
* Some methods return an immediate object as the msg. | ||
* For example: group_getById outputs: | ||
* { | ||
* code: 0, | ||
* msg: { id: '60', parentId: '0', name: 'test', description: '', ... } | ||
* } | ||
* | ||
* But group_getByObject outputs: | ||
* { | ||
* code: 0, | ||
* msg: { | ||
* Group: [ | ||
* { id: '60', parentId: '0', name: 'test', description: ', ... }, | ||
* ... | ||
* ] | ||
* } | ||
* } | ||
* And some methods like customer_delete output: | ||
* { | ||
* code: 0, | ||
* msg: true | ||
* } | ||
*/ | ||
const getOutput = (clang, clangMethodName, data) => { | ||
const outputDefinition = clang.description.clangAPI.clangPort[clangMethodName].output | ||
outputDefinition.outputType = outputDefinition.outputType || (() => { | ||
let outputKeys, match | ||
if (typeof outputDefinition.msg === 'string') {// like "xsd:boolean" | ||
return 'primitive' | ||
} | ||
outputKeys = Object.keys(outputDefinition.msg) | ||
.filter(key=>( | ||
key !== 'targetNSAlias' && | ||
key !== 'targetNamespace' | ||
)) | ||
if (outputKeys.length === 1 && (match = outputKeys[0].match(/(.*)\[\]$/))) { | ||
// Matches Group[] | ||
return match[1] | ||
} | ||
return 'object' | ||
})() | ||
if (outputDefinition.outputType === 'primitive') { | ||
return data | ||
} | ||
if (outputDefinition.outputType === 'object') { | ||
return data | ||
} | ||
return {[outputDefinition.outputType]: data} | ||
} |
{ | ||
"name": "clang", | ||
"version": "0.2.10", | ||
"version": "1.0.1", | ||
"description": "Node.js api wrapper for Clang's SOAP api", | ||
@@ -11,22 +11,33 @@ "author": "Christiaan Westerbeek <chris@devotis.nl>", | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/devotis/clang.git" | ||
"scripts": { | ||
"pretest": "npm run lint", | ||
"test": "node ./test", | ||
"lint": "eslint --ext=.js ." | ||
}, | ||
"main": "./index.js", | ||
"directories": { | ||
"lib": "./lib" | ||
}, | ||
"engines": { | ||
"node": ">=4.4.5" | ||
"node": ">=6.5" | ||
}, | ||
"dependencies": { | ||
"async": "2.4.1", | ||
"lodash": "4.17.4", | ||
"moment": "2.18.1", | ||
"prompt": "1.0.0", | ||
"soap": "0.19.2" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^3.4.2", | ||
"should": "^11.2.1" | ||
"config": "1.26.1", | ||
"eslint": "4.1.1", | ||
"tape": "4.6.3" | ||
}, | ||
"directories": { | ||
"lib": "./lib" | ||
"homepage": "https://github.com/devotis/clang", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/devotis/clang.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/devotis/clang/issues" | ||
}, | ||
"licenses": [ | ||
@@ -37,10 +48,3 @@ { | ||
} | ||
], | ||
"homepage": "https://github.com/devotis/clang", | ||
"bugs": { | ||
"url": "https://github.com/devotis/clang/issues" | ||
}, | ||
"scripts": { | ||
"test": "mocha" | ||
} | ||
] | ||
} |
@@ -7,32 +7,41 @@ [![Build Status](https://travis-ci.org/devotis/node-clang.svg)](https://travis-ci.org/devotis/node-clang) | ||
Anyone using <a href="http://www.createaclang.com/">Clang</a>'s SOAP api? Here's something that will make your life easier. A Node.js api that wraps Clang's SOAP api. You don't need to bother with the extra resource callbacks that some methods like customer_getByObject require. Internally these resources are polled and the final dataset is returned in the callback. | ||
Using <a href="http://www.createaclang.com/">Clang</a>'s SOAP api? Here's a module that will make your life easier. A Node.js api that wraps Clang's SOAP api. You don't need to bother with the extra resource callbacks that some methods like customer_getByObject require. Internally these resources are polled and the final dataset is returned in the callback. | ||
I had a ton of fun building this. Node.js rocks! | ||
##Install | ||
## Install | ||
npm install clang | ||
```javascript | ||
npm install clang | ||
``` | ||
##Example | ||
## 1.0 breaking changes | ||
- instantiation without new keyword is not possible anymore. | ||
- config.normalizeOptionFields does not default to true anymore. It doesn't default at all. | ||
- config.logRequests is renamed to config.logPayload and also logs raw xml output | ||
- customer_getById and similar methods that would normally output an object have always been wrapped in an array by this module. From 1.0 on when single objects are expected they are no longer wrapped in an array. methods that would normally output an array like customer_getAll, but output just one because of the available data (and/or filter set) continue to be wrapped in an array. | ||
var Clang = require("./lib/clang"); | ||
## Example | ||
var clang = new Clang({uuid: '12345678-1234-1234-1234-123456781234'}); | ||
```javascript | ||
const Clang = require('clang') | ||
const clang = new Clang({uuid: '12345678-1234-1234-1234-123456781234'}) | ||
clang.request('group_getMembers', { | ||
groupId: 2 | ||
}, function(err, result) { | ||
console.log(arguments); | ||
}); | ||
clang.request('group_getMembers', { | ||
groupId: 2 | ||
}, (err, result) => { | ||
console.log(err, result) | ||
}) | ||
//Paging using the special parameters _size and _offset. Defaults are 50 and 0 | ||
clang.request('customer_getAll', { | ||
_size: 10 | ||
}, function(err, result) { | ||
console.log(arguments); | ||
}); | ||
// Paging using the special parameters _size and _offset. Defaults are 50 and 0 | ||
clang.request('customer_getAll', { | ||
_size: 10 | ||
}, (err, result) => { | ||
console.log(err, result) | ||
}) | ||
``` | ||
##WSDL | ||
This is the underlying WSDL document that is used | ||
## WSDL | ||
This is the underlying WSDL document. | ||
https://secure.myclang.com/app/api/soap/public/index.php?wsdl |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
626084
24
9670
0
47
4
3
7
1
+ Addedmoment@2.18.1
+ Addedprompt@1.0.0
+ Addedasync@0.9.21.0.0(transitive)
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedcolors@1.0.31.4.0(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addedcycle@1.0.3(transitive)
+ Addeddeep-equal@0.2.2(transitive)
+ Addedeyes@0.1.8(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedglob@7.2.3(transitive)
+ Addedi@0.3.7(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedminimatch@3.1.2(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedmoment@2.18.1(transitive)
+ Addedmute-stream@0.0.8(transitive)
+ Addedncp@1.0.1(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedpkginfo@0.3.10.4.1(transitive)
+ Addedprompt@1.0.0(transitive)
+ Addedread@1.0.7(transitive)
+ Addedrevalidator@0.1.8(transitive)
+ Addedrimraf@2.7.1(transitive)
+ Addedstack-trace@0.0.10(transitive)
+ Addedutile@0.3.0(transitive)
+ Addedwinston@2.1.1(transitive)
+ Addedwrappy@1.0.2(transitive)
- Removedlodash@4.17.4
- Removedlodash@4.17.4(transitive)