UNPKG

mailchimp-api-v3

Version:

Mailchimp wrapper for v3 of the mailchimp api, with transparant handling of batch operations

554 lines (420 loc) 11.8 kB
"use strict"; var request = require('request'), tar = require('tar'), zlib = require('zlib'), Promise = require("bluebird"), _ = require('lodash'); Promise.config({ // Enables all warnings except forgotten return statements. warnings: { wForgottenReturn: false } }); function Mailchimp (api_key, dc = null) { var api_key_regex = /.+\-.+/ if (!api_key_regex.test(api_key) && dc === null) { throw new Error('missing or invalid api key: ' + api_key) } this.__api_key = api_key; if(dc !== null){ this.__base_url = "https://"+ dc + ".api.mailchimp.com/3.0" }else{ this.__base_url = "https://"+ this.__api_key.split('-')[1] + ".api.mailchimp.com/3.0" } } var formatPath = function (path, path_params) { if (!path) { path = '/'; } if (path[0] != '/') { path = '/' + path; } if (!path_params) { return path; } if (_.isEmpty(path_params)) { return path; } path = _.reduce(path_params, function (_path, value, param) { return _path.replace('{'+param+'}', value); }, path) return path; } Mailchimp.prototype.get = function (options, query, done) { options = _.clone(options) || {}; if (_.isString(options)) { options = { path : options, } } options.method = 'get'; if (!done && _.isFunction(query)) { done = query; query = null; } if (query && options.query) { console.warn('query set on request options overwritten by argument query'); } if (query) { options.query = query; } return this.request(options, done); } Mailchimp.prototype.post = function (options, body, done) { options = _.clone(options) || {}; if (_.isString(options)) { options = { path : options, } } options.method = 'post'; if (!done && _.isFunction(body)) { done = body; body = null; } if (body && options.body) { console.warn('body set on request options overwritten by argument body'); } if (body) { options.body = body; } return this.request(options, done); } Mailchimp.prototype.patch = function (options, body, done) { options = _.clone(options) || {}; if (_.isString(options)) { options = { path : options, } } options.method = 'patch'; if (!done && _.isFunction(body)) { done = body; body = null; } if (body && options.body) { console.warn('body set on request options overwritten by argument body'); } if (body) { options.body = body; } return this.request(options, done); } Mailchimp.prototype.put = function (options, body, done) { options = _.clone(options) || {}; if (_.isString(options)) { options = { path : options, } } options.method = 'put'; if (!done && _.isFunction(body)) { done = body; body = null; } if (body && options.body) { console.warn('body set on request options overwritten by argument body'); } if (body) { options.body = body; } return this.request(options, done); } Mailchimp.prototype.delete = function (options, done) { options = options || {}; options = _.clone(options) if (_.isString(options)) { options = { path : options, } } options.method = 'delete'; return this.request(options, done); } Mailchimp.prototype._getAndUnpackBatchResults = function (response_body_url, opts) { return new Promise(function (resolve, reject) { var parse = new tar.Parse(); var results = []; parse.on('entry', function(entry){ if (!entry.path.match(/\.json/)){ entry.resume(); return } var result_json = ''; entry.on('data', function (data) { result_json += data.toString(); }) entry.on('error', function (err) { parse.close(); entry.close(); reject(new Error(err)); }) entry.on('end', function () { results.push(JSON.parse(result_json)); }) }); parse.on('error', function (err) { parse.close(); reject(new Error(err)); }) parse.on('end', function (res) { results = _.flatten(results); //TODO: implement linear sort uding operation id is linear from 0 to length-1 results.sort(function (result_a, result_b) { return result_a.operation_id - result_b.operation_id }) for (var i = 0; i < results.length; i++) { results[i] = JSON.parse(results[i].response); }; resolve(results) }) request.get({ url : response_body_url, encoding : null }, function (err, response) { if (err) { reject(new Error(err)); return; } if (response.statusCode != 200) { reject(Object.assign(new Error(), response.body)); return; } var response_buffer = response.body; zlib.gunzip(response_buffer, function (err, result) { if (err) { reject(new Error(err)); return; } parse.end(result) }) }) }) } Mailchimp.prototype.batchWait = function (batch_id, done, opts) { var mailchimp = this; //If done is not a function, and no opts are given, second argument is the opts if (!opts && !_.isFunction(done)) { opts = done; done = null; } opts = _.clone(opts) || {}; if (!opts.interval) { opts.interval = 2000 } //default unpack to true if (opts.unpack !== false) { opts.unpack = true; } //default verbose to true if (opts.verbose !== false) { opts.verbose = true; } var options = { method : 'get', path : '/batches/' + batch_id } var promise = new Promise(function (resolve, reject) { var request = function () { mailchimp.request(options) .then(function (result) { if (opts.verbose) { console.log('batch status:', result.status, result.finished_operations + '/' + result.total_operations) } if (result.status == 'finished') { resolve(result); return; } setTimeout(request, opts.interval); }, reject) } request(); }) if (opts.unpack) { promise = promise.then(function (result) { //in case the batch was empty, there is nothing to unpack (should no longer be hit) if (result.total_operations == 0) { return []; } return mailchimp._getAndUnpackBatchResults(result.response_body_url, opts) }) } //If a callback is used, resolve it and don't return the promise if (done) { promise .then(function (result) { done(null, result) }) .catch(function (err) { done(err); }) return null; } return promise } Mailchimp.prototype.batch = function (operations, done, opts) { var mailchimp = this; //If done is not a function, and no opts are given, second argument is the opts if (!opts && !_.isFunction(done)) { opts = done; done = null; } opts = _.clone(opts) || {}; //TODO: Validate arguments and reject errors //If the batch call does not get an operation, but a single normal call, return the result instead of a length 1 array //This is useful for large get requests, like all subscribers of a list without paging var should_unarray = false; if (!_.isArray(operations)) { operations = [operations] should_unarray = true; } //default wait to true if (opts.wait !== false) { opts.wait = true; } //default unpack to true if (opts.unpack !== false) { opts.unpack = true; } //default verbose to true if (opts.verbose !== false) { opts.verbose = true; } //handle special case of empty batch with unpack. //empty batches without unpack are still sent to mailchimp to get consistent responses from mailchimp if (operations.length == 0 && opts.wait && opts.unpack) { return Promise.resolve([]); } var _operations = []; var id = 0; _.each(operations, function (operation) { var _operation = _.clone(operation); _operation.operation_id = id.toString(); if (_operation.body) { _operation.body = JSON.stringify(_operation.body); } _operation.path = formatPath(_operation.path, _operation.path_params); if (_operation.method) { _operation.method = _operation.method.toUpperCase(); } if (_operation.query) { _operation.params = _.assign({},_operation.query, _operation.params); delete _operation.query } _operations.push(_operation); id++; }) var promise = mailchimp.request({ method : 'post', path : '/batches', body : { operations : _operations } }) if (opts.verbose) { promise = promise.then(function (result) { console.log('Batch started with id:', result.id); return result }) } if (opts.wait) { promise = promise.then(function (result) { return mailchimp.batchWait(result.id, opts) }) } if (opts.wait && opts.unpack && should_unarray) { promise = promise.then(function (result) { if (result.length == 1) { result = result[0]; } return result }) } //If a callback is used, resolve it and don't return the promise if (done) { promise .then(function (result) { done(null, result) }) .catch(function (err) { done(err); }) return null; } return promise } Mailchimp.prototype.request = function (options, done) { var mailchimp = this; var promise = new Promise(function(resolve, reject) { if (!options) { reject(new Error("No request options given")); return; } var path = formatPath(options.path, options.path_params); var method = options.method || 'get'; var body = options.body || {}; var params = options.params; var query = options.query; var headers = { 'User-Agent' : 'mailchimp-api-v3 : https://github.com/thorning/node-mailchimp' }; // Mailchimp does not respect on the language set in requests bodies for confirmation emails on new subscribers (and maybe other) // A workaround is to make sure the language header matches var language = options.language || body.language || null; if (language) { headers['Accept-Language'] = language; } //Parems used to refer to query parameters, because of the mailchimp documentation. if (params) { if (!query) { query = params; } } if (!path || !_.isString(path)) { reject(new Error('No path given')) return; } request({ method : method, url : mailchimp.__base_url + path, auth : { user : 'any', password : mailchimp.__api_key }, json : body, qs : query, headers : headers, }, function (err, response) { if (err) { var error = new Error(err); error.response = response; error.statusCode = response ? response.statusCode : undefined; reject(error) return; } if (response.statusCode < 200 || response.statusCode > 299) { var error = Object.assign(new Error(response.body ? response.body.detail : response.statusCode), response.body || response) error.response = response; error.statusCode = response.statusCode; reject(error); return; } var result = response.body || {}; result.statusCode = response.statusCode; resolve(result) }) }) //If a callback is used, resolve it and don't return the promise if (done) { promise .then(function (result) { done(null, result) }) .catch(function (err) { done(err); }) return null; } return promise } module.exports = exports = Mailchimp;