UNPKG

mws-sdk2

Version:

Amazon Marketplace Web Services client with support for all api calls.

536 lines (481 loc) 15.8 kB
var https = require('https'), qs = require("querystring"), crypto = require('crypto'), tls = require('tls'); xml2js = require('xml2js'); var iconv = require('iconv-lite'); var MARKETPLACE_IDS = { }; /** * Constructor for the main MWS client interface used to make api calls and * various data structures to encapsulate MWS requests, definitions, etc. * * @param {String} accessKeyId Id for your secret Access Key (required) * @param {String} secretAccessKey Secret Access Key provided by Amazon (required) * @param {String} merchantId Aka SellerId, provided by Amazon (required) * @param {Object} options Additional configuration options for this instance */ function AmazonMwsClient(accessKeyId, secretAccessKey, merchantId, authToken, options) { this.host = options.host || 'mws.amazonservices.com'; this.port = options.port || 443; this.conn = options.conn || https; this.debug = options.debug || false; if (tls.createSecureContext) { this.creds = tls.createSecureContext(options.creds || {}); } else { this.creds = crypto.createCredentials(options.creds || {}); } this.appName = options.appName || 'mws-js'; this.appVersion = options.appVersion || '0.1.0'; this.appLanguage = options.appLanguage || 'JavaScript'; this.accessKeyId = accessKeyId || null; this.authToken = authToken || null; this.secretAccessKey = secretAccessKey || null; this.merchantId = merchantId || null; } /** * The method used to invoke calls against MWS Endpoints. Recommended usage is * through the invoke wrapper method when the api call you're invoking has a * request defined in one of the submodules. However, you can use call() manually * when a lower level of control is necessary (custom or new requests, for example). * * @param {Object} api Settings object unique to each API submodule * @param {String} action Api `Action`, such as GetServiceStatus or GetOrder * @param {Object} query Any parameters belonging to the current action * @param {Function} callback Callback function to send any results recieved */ AmazonMwsClient.prototype.call = function(api, action, query, callback) { if (this.secretAccessKey == null || this.accessKeyId == null || this.merchantId == null) { throw("accessKeyId, secretAccessKey, and merchantId must be set"); } if(this.debug) { console.log("=================== API ==================="); console.log(JSON.stringify(api)); console.log("================ QUERY =============="); console.log(JSON.stringify(query)); console.log("=============================="); } var buffers = []; var buffer; var self = this; // Add required parameters and sign the query query['Action'] = action; query['Version'] = api.version; query["Timestamp"] = (new Date()).toISOString(); query["AWSAccessKeyId"] = self.accessKeyId; query['MWSAuthToken'] = self.authToken; if (api.legacy) { query['Merchant'] = self.merchantId; } else { query['SellerId'] = self.merchantId; } if (api.upload) { var body = query._BODY_, bformat = query._FORMAT_; delete query._BODY_; delete query._FORMAT_; } // Check if we're dealing with a file (such as a feed) upload if (api.upload && body !== null && typeof body === 'object' && typeof body.pipe === 'function') { // last three conditions to validate if it's a stream. body.on('data', function(buffer) { buffers.push(buffer); }); body.on('end', function(){ buffer = Buffer.concat(buffers); if(self.debug){ console.log('before query : '); console.log(query); } query = self.sign(api.path, query); if(self.debug){ console.log('query : '); console.log(query); } // Setup our HTTP headers and connection options var headers = { 'Host': self.host, 'User-Agent': self.appName + '/' + self.appVersion + ' (Language=' + self.appLanguage + ')', 'Content-Type': bformat || 'text/tab-separated-values; charset=iso-8859-1', 'Content-Length': buffer.length }; headers['Content-MD5'] = crypto.createHash('md5').update(buffer).digest("base64"); var pathReq = api.path + (api.upload ? '?' + qs.stringify(query) : ''); if(self.debug){ console.log("************ ACTUAL PATH: ***************"); console.log(pathReq); } var options = { host: self.host, port: self.port, path: pathReq, method: "POST", headers: headers }; sendRequest(self, options, buffer, function(err, result) { callback(err, result); }); }); } else { if(self.debug){ console.log('query : '); console.log(query); } query = this.sign(api.path, query); if (!api.upload) { var body = qs.stringify(query); } // Setup our HTTP headers and connection options var headers = { 'Host': this.host, 'User-Agent': this.appName + '/' + this.appVersion + ' (Language=' + this.appLanguage + ')', 'Content-Type': bformat || 'application/x-www-form-urlencoded; charset=utf-8', //'Content-Type': 'text/tab-separated-values; charset=iso-8859-1', 'Content-Length': Buffer.byteLength(body) }; if (api.upload) { headers['Content-MD5'] = crypto.createHash('md5').update(body).digest('base64'); } var pathReq = api.path + (api.upload ? '?' + qs.stringify(query) : ''); if(self.debug){ console.log("************ ACTUAL PATH: ***************"); console.log(pathReq); } var options = { host: this.host, port: this.port, path: pathReq, method: "POST", headers: headers }; if(self.debug){ console.log(options); } sendRequest(this, options, body, function(err, result) { callback(err, result); }); } }; function sendRequest(self, options, body, callback) { // Make the initial request and define callbacks var req = self.conn.request(options, function (res) { var data = ''; res.setEncoding('binary'); if(self.debug){ console.log(res.headers); } // Append each incoming chunk to data variable res.addListener('data', function (chunk) { var utf8Chunk = iconv.decode(new Buffer(chunk), 'utf8'); data += utf8Chunk.toString(); if(self.debug){ console.log('data'); console.log(data); } }); // When response is complete, parse the XML and pass it to callback res.addListener('end', function() { var parser = new xml2js.Parser(); parser.addListener('end', function (result) { // Throw an error if there was a problem reported if (result.Error != null) { if(self.debug) { console.log("--------------------------------"); console.log("ERROR:"); console.log(JSON.stringify(result.Error)); } throw("ERROR"); } callback(null, result); }); if (data.slice(0, 5) == '<?xml') parser.parseString(data); else callback(null, data); }); }); req.on('error', function(err) { if(self.debug){ console.log('errorInvoke'); console.log(err); } callback(err); }); req.write(body, 'utf8'); req.end(); }; /** * Calculates the HmacSHA256 signature and appends it with additional signature * parameters to the provided query object. * * @param {String} path Path of API call (used to build the string to sign) * @param {Object} query Any non-signature parameters that will be sent * @return {Object} Finalized object used to build query string of request */ AmazonMwsClient.prototype.sign = function(path, query) { var keys = [], sorted = {}, hash = crypto.createHmac("sha256", this.secretAccessKey); // Configure the query signature method/version query["SignatureMethod"] = "HmacSHA256"; query["SignatureVersion"] = "2"; // Copy query keys, sort them, then copy over the values for(var key in query) keys.push(key); keys = keys.sort(); for(n in keys) { var key = keys[n]; sorted[key] = query[key]; } var stringToSign = ["POST", this.host, path, qs.stringify(sorted)].join("\n"); if(this.debug){ console.log(stringToSign); } // An RFC (cannot remember which one) requires these characters also be changed: stringToSign = stringToSign.replace(/'/g,"%27"); stringToSign = stringToSign.replace(/\*/g,"%2A"); stringToSign = stringToSign.replace(/\(/g,"%28"); stringToSign = stringToSign.replace(/\)/g,"%29"); query['Signature'] = hash.update(stringToSign).digest("base64"); return query; }; /** * Suggested method for invoking a pre-defined mws request object. * * @param {Object} request An instance of AmazonMwsRequest with params, etc. * @param {Function} callback Callback function used to process results/errors */ AmazonMwsClient.prototype.invoke = function(request, callback) { if(this.debug) { console.log('++++++++++++++++++++++++++++++++++++'); console.log('INVOKE'); console.log(JSON.stringify(request)); } this.call(request.api, request.action, request.query(), callback); }; /** * Constructor for general MWS request objects, wrapped by api submodules to keep * things DRY, yet familiar despite whichever api is being implemented. * * @param {Object} options Settings to apply to new request instance. */ function AmazonMwsRequest(options) { this.api = { path: options.path || '/', version: options.version || '2009-01-01', legacy: options.legacy || false, upload: options.upload }; this.debug = options.debug || false; this.action = options.action || 'GetServiceStatus'; this.params = options.params || {}; } /** * Handles the casting, renaming, and setting of individual request params. * * @param {String} param Key of parameter (not ALWAYS the same as the param name!) * @param {Mixed} value Value to assign to parameter * @return {Object} Current instance to allow function chaining */ AmazonMwsRequest.prototype.set = function(param, value) { if(this.debug){ console.log(param); } var p = this.params[param], v = p.value = {}; // Handles the actual setting based on type var setValue = function(name, val) { if (p.type == 'Timestamp') { v[name] = val.toISOString(); } else if (p.type == 'Boolean') { v[name] = val ? 'true' : 'false'; } else if (p.list) { v[name] = val; } else { v[name] = val; } } // Lists need to be sequentially numbered and we take care of that here if (p.list) { var i = 0; if ((typeof(value) == "string") || (typeof(value) == "number")) { setValue(p.name + '.1', value); } if (typeof(value) == "object") { if (Array.isArray(value)) { for (i = value.length - 1; i >= 0; i--) { setValue(p.name + '.' + (i+1), value[i]); } } else { for (var key in value) { setValue(p.name + '.' + (++i), value[key]); } } } } else { setValue(p.name, value) } return this; }; /** * Builds a query object and checks for required parameters. * * @return {Object} KvP's of all provided parameters (used by invoke()) */ AmazonMwsRequest.prototype.query = function() { var q = {}; if(this.debug) { console.log("AMR.proto.query !!!!!!!!!!!!!!!!!!!!!!!"); console.log(JSON.stringify(this)); console.log(JSON.stringify(this.params)); } for (var param in this.params) { var value = this.params[param].value, name = this.params[param].name, complex = (this.params[param].type === 'Complex'), required = this.params[param].required; if(this.debug){ console.log("v " + value + "\nn " + name + "\nr " + required); } if ((value !== undefined) && (value !== null)) { if (complex) { value.appendTo(q); } else if (this.params[param].list){ for (var key in value) { q[key] = value[key]; } } else { for (key in value) { q[key] = value[key]; } } } else { if (param.required === true) { throw("ERROR: Missing required parameter, " + name + "!") } } }; if(this.debug) { console.log("query() about to return this:"); console.log(q); console.log("----------------------------------"); } return q; }; /** * Contructor for objects used to represent enumeration states. Useful * when you need to make programmatic updates to an enumerated data type or * wish to encapsulate enum states in a handy, re-usable variable. * * @param {Array} choices An array of any possible values (choices) */ function EnumType(choices) { for (var choice in choices) { this[choices[choice]] = false; } this._choices = choices; } /** * Enable one or more choices (accepts a variable number of arguments) * @return {Object} Current instance of EnumType for chaining */ EnumType.prototype.enable = function() { for (var arg in arguments) { this[arguments[arg]] = true; } return this; }; /** * Disable one or more choices (accepts a variable number of arguments) * @return {Object} Current instance of EnumType for chaining */ EnumType.prototype.disable = function() { for (var arg in arguments) { this[arguments[arg]] = false; } return this; }; /** * Toggles one or more choices (accepts a variable number of arguments) * @return {Object} Current instance of EnumType for chaining */ EnumType.prototype.toggle = function() { for (var arg in arguments) { this[arguments[arg]] = ! this[arguments[arg]]; } return this; }; /** * Return all possible values without regard to current state * @return {Array} Choices passed to EnumType constructor */ EnumType.prototype.all = function() { return this._choices; }; /** * Return all enabled choices as an array (used to set list params, usually) * @return {Array} Choice values for each choice set to true */ EnumType.prototype.values = function() { var value = []; for (var choice in this._choices) { if (this[this._choices[choice]] === true) { value.push(this._choices[choice]); } } return value; }; // /** // * Takes an object and adds an appendTo function that will add // * each kvp of object to a query. Used when dealing with complex // * parameters that need to be built in an abnormal or unique way. // * // * @param {String} name Name of parameter, prefixed to each key // * @param {Object} obj Parameters belonging to the complex type // */ // function ComplexType(name) { // this.pre = name; // var _obj = obj; // obj.appendTo = function(query) { // for (var k in _obj) { // query[name + '.' k] = _obj[k]; // } // return query; // } // return obj; // } // ComplexType.prototype.appendTo = function(query) { // for (var k in value) // } /** * Complex List helper object. Once initialized, you should set * an add(args) method which pushes a new complex object to members. * * @param {String} name Name of Complex Type (including .member or subtype) */ function ComplexListType(name) { this.pre = name; this.members = []; } /** * Appends each member object as a complex list item * @param {Object} query Query object to append to * @return {Object} query */ ComplexListType.prototype.appendTo = function(query) { var members = this.members; for (var i = 0; i < members.length; i++) { for (var j in members[i]) { query[this.pre + '.' + (i+1) + '.' + j] = members[i][j] } } return query; }; exports.Client = AmazonMwsClient; exports.Request = AmazonMwsRequest; exports.Enum = EnumType; exports.ComplexList = ComplexListType; exports.Orders = require('./orders'); exports.Sellers = require('./sellers'); exports.Feeds = require('./feeds'); exports.Products = require('./products'); exports.Reports = require('./reports');