diff --git a/lib/client.js b/lib/client.js
index 9106023..5d4c466 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -43,7 +43,7 @@
// 200–299 success
if (res.statusCode >= 200 && res.statusCode <= 299) {
if (responsedata === '') {
- return _cb(res);
+ return _cb(res, new Error('Response has no data'));
}
return parser.parseString(responsedata, function (err, result) {
return _cb(res, null, result);
@@ -83,7 +83,7 @@
}
if (callback.length === 2) {
if (err) {
- return callback(_wrap_response(res, err));
+ return callback(err, _wrap_response(res, null));
}
return callback(null, _wrap_response(res, data));
diff --git a/lib/recurly.js b/lib/recurly.js
index 81e4cce..074d059 100644
--- a/lib/recurly.js
+++ b/lib/recurly.js
@@ -2,6 +2,8 @@
var Js2xml = require('js2xml').Js2Xml,
Client = require('./client'),
utils = require('./utils'),
+ _ = require('lodash'),
+ url = require('url'),
router = require('./routes/');
module.exports = function(config){
@@ -13,6 +15,7 @@
list: function(callback, filter){
t.request(utils.addQueryParams(routes.accounts.list, filter), callback);
},
+ eachSeries: utils.makeEachSeriesIterator(t, routes.accounts.list, 'accounts', 'account'),
get: function(accountcode, callback){
t.request(utils.addParams(routes.accounts.get, {account_code: accountcode}), callback);
},
@@ -26,9 +29,9 @@
t.request(utils.addParams(routes.accounts.close, {account_code: accountcode}), callback);
},
reopen: function(accountcode, callback){
- t.request(utils.addParams(routes.accounts.reopen, {account_code: accountcode}), callback)
+ t.request(utils.addParams(routes.accounts.reopen, {account_code: accountcode}), callback);
}
- }
+ };
this.adjustments = {
get: function(accountcode, callback){
@@ -40,7 +43,7 @@
remove: function(uuid, callback){
t.request(utils.addParams(routes.adjustments.remove, {uuid: uuid}), callback);
}
- }
+ };
//http://docs.recurly.com/api/billing-info
this.billingInfo = {
@@ -53,13 +56,14 @@
remove: function(accountcode, callback){
t.request(utils.addParams(routes.billingInfo.remove, {account_code: accountcode} ), callback);
}
- }
+ };
//http://docs.recurly.com/api/coupons
this.coupons = {
list: function(callback, filter){
t.request(utils.addQueryParams(routes.coupons.list, filter), callback);
},
+ eachSeries: utils.makeEachSeriesIterator(t, routes.coupons.list, 'coupons', 'coupon'),
get: function(couponcode, callback){
t.request(utils.addParams(routes.coupons.get, {coupon_code: couponcode}), callback);
},
@@ -69,7 +73,7 @@
deactivate: function(couponcode, callback){
t.request(utils.addParams(routes.coupons.deactivate, {coupon_code: couponcode}), callback);
}
- }
+ };
this.couponRedemption = {
redeem: function(couponcode, details, callback){
@@ -82,20 +86,21 @@
t.request(utils.addParams(routes.couponRedemption.remove, {account_code: accountcode}), callback);
},
getByInvoice: function(invoicenumber, callback){
- t.request(utils.addParams(routes.couponRedemption.getByInvoice, {invoice_number: invoicenumber}), callback)
+ t.request(utils.addParams(routes.couponRedemption.getByInvoice, {invoice_number: invoicenumber}), callback);
}
- }
+ };
this.invoices = {
list: function(callback, filter){
t.request(utils.addQueryParams(routes.invoices.list, filter), callback);
},
+ eachSeries: utils.makeEachSeriesIterator(t, routes.invoices.list, 'invoices', 'invoice'),
listByAccount: function(accountcode, callback, filter){
t.request(
utils.addParams(
- utils.addQueryParams(routes.invoices.listByAccount, filter)
- , {account_code: accountcode})
- , callback)
+ utils.addQueryParams(routes.invoices.listByAccount, filter),
+ {account_code: accountcode}),
+ callback);
},
get: function(invoicenumber, callback){
t.request(utils.addParams(routes.invoices.get, {invoice_number: invoicenumber}), callback);
@@ -109,12 +114,13 @@
markFailed: function(invoicenumber, callback){
t.request(utils.addParams(routes.invoices.markFailed, {invoice_number: invoicenumber}), callback);
}
- }
+ };
this.plans = {
list: function(callback, filter){
t.request(utils.addQueryParams(routes.plans.list, filter), callback);
},
+ eachSeries: utils.makeEachSeriesIterator(t, routes.plans.list, 'plans', 'plan'),
get: function(plancode, callback){
t.request(utils.addParams(routes.plans.get, {plan_code: plancode}), callback);
},
@@ -127,12 +133,13 @@
remove: function(plancode, callback){
t.request(utils.addParams(routes.plans.remove, {plan_code: plancode}), callback);
}
- }
+ };
this.planAddons = {
list: function(plancode, callback, filter){
t.request(utils.addParams(utils.addQueryParams(routes.planAddons.list, filter), {plan_code: plancode}), callback);
},
+ eachSeries: utils.makeEachSeriesIterator(t, routes.planAddons.list, 'add_ons', 'ad_on'),
get: function(plancode, addoncode, callback){
t.request(utils.addParams(routes.planAddons.get, {plan_code: plancode, addon_code: addoncode}), callback);
},
@@ -141,24 +148,24 @@
},
update: function(plancode, addoncode, details, callback){
t.request(utils.addParams(
- routes.planAddons.update,
+ routes.planAddons.update,
{ plan_code: plancode,
add_on_code: addoncode
- }),
+ }),
callback, new Js2xml('add_on', details).toString());
},
remove: function(plancode, addoncode, callback){
t.request(utils.addParams(routes.planAddons.remove, {plan_code: plancode, add_on_code: addoncode}), callback);
}
- }
+ };
this.subscriptions = {
list: function(callback, filter){
t.request(utils.addQueryParams(routes.subscriptions.list, filter), callback);
},
+ eachSeries: utils.makeEachSeriesIterator(t, routes.subscriptions.list, 'subscriptions', 'subscription'),
listByAccount: function(accountcode, callback){
t.request(utils.addParams(routes.subscriptions.listByAccount, {account_code: accountcode}), callback);
-
},
get: function(uuid, callback){
t.request(utils.addParams(routes.subscriptions.get, {uuid: uuid}), callback);
@@ -181,12 +188,13 @@
postpone: function(uuid, nextRenewalDate, callback){
t.request(utils.addParams(routes.subscriptions.postpone, {uuid: uuid, next_renewal_date: nextRenewalDate}), callback);
}
- }
+ };
this.transactions = {
list: function(callback, filter){
t.request(utils.addQueryParams(routes.transactions.list, filter), callback);
},
+ eachSeries: utils.makeEachSeriesIterator(t, routes.transactions.list, 'transactions', 'transaction'),
listByAccount: function(accountCode, callback, filter){
t.request(
utils.addParams(
@@ -204,8 +212,8 @@
if(amount){
route = utils.addQueryParams(route, { amount_in_cents: amount });
}
- t.request(route, callback)
+ t.request(route, callback);
}
- }
- }//end class
+ };
+ }; //end class
})();
diff --git a/lib/routes/index.js b/lib/routes/index.js
index 91cf80d..d40a54f 100644
--- a/lib/routes/index.js
+++ b/lib/routes/index.js
@@ -1,5 +1,5 @@
var routes = {
- 'v2': require('./v2')
+ 'v2': require('./v2')
};
diff --git a/lib/routes/v2.js b/lib/routes/v2.js
index d3fa0f6..93f4b22 100644
--- a/lib/routes/v2.js
+++ b/lib/routes/v2.js
@@ -5,33 +5,33 @@ exports.accounts = {
update: ['/v2/accounts/:account_code', 'PUT'],
close: ['/v2/accounts/:account_code', 'DELETE'],
reopen: ['/v2/accounts/:account_code/reopen', 'PUT']
-}
+};
exports.adjustments = {
get: ['/v2/accounts/:account_code/adjustments', 'GET'],
create: ['/v2/accounts/:account_code/adjustments', 'POST'],
remove: ['/v2/adjustments/:uuid', 'DELETE']
-}
+};
exports.billingInfo = {
get: ['/v2/accounts/:account_code/billing_info', 'GET'],
update: ['/v2/accounts/:account_code/billing_info', 'PUT'],
remove: ['/v2/accounts/:account_code/billing_info', 'DELETE']
-}
+};
exports.coupons = {
list: ['/v2/coupons', 'GET'],
get: ['/v2/coupons/:coupon_code', 'GET'],
create: ['/v2/coupons', 'POST'],
deactivate: ['/v2/coupons/:coupon_code', 'DELETE']
-}
+};
exports.couponRedemption = {
redeem: ['/v2/coupons/:coupon_code/redeem', 'POST'],
get: ['/v2/accounts/:account_code/redemption', 'GET'],
remove: ['/v2/accounts/:account_code/redemption', 'DELETE'],
getByInvoice: ['/v2/invoices/:invoice_number/redemption', 'GET']
-}
+};
exports.invoices = {
list: ['/v2/invoices', 'GET'],
@@ -40,7 +40,7 @@ exports.invoices = {
create: ['/v2/accounts/:account_code/invoices', 'POST'],
markSuccessful: ['/v2/invoices/:invoice_number/mark_successful', 'PUT'],
markFailed: ['/v2/invoices/:invoice_number/mark_failed', 'PUT']
-}
+};
exports.plans = {
list: ['/v2/plans', 'GET'],
@@ -48,7 +48,7 @@ exports.plans = {
create: ['/v2/plans', 'POST'],
update: ['/v2/plans/:plan_code', 'PUT'],
remove: ['/v2/plans/:plan_code', 'DELETE']
-}
+};
exports.planAddons = {
list: ['/v2/plans/:plan_code/add_ons', 'GET'],
@@ -56,7 +56,7 @@ exports.planAddons = {
create: ['/v2/plans/:plan_code/add_ons', 'POST'],
update: ['/v2/plans/:plan_code/add_ons/:add_on_code', 'PUT'],
remove: ['/v2/plans/:plan_code/add_ons/:add_on_code', 'DELETE']
-}
+};
exports.subscriptions = {
list: ['/v2/subscriptions', 'GET'],
@@ -68,7 +68,7 @@ exports.subscriptions = {
reactivate: ['/v2/subscriptions/:uuid/reactivate', 'PUT'],
terminate: ['/v2/subscriptions/:uuid/terminate?refund=:refund_type', 'PUT'],
postpone: ['/v2/subscriptions/:uuid/postpone?next_renewal_date=:next_renewal_date', 'PUT']
-}
+};
exports.transactions = {
list: ['/v2/transactions', 'GET'],
@@ -76,4 +76,4 @@ exports.transactions = {
get: ['/v2/transactions/:id', 'GET'],
create: ['/v2/transactions', 'POST'],
refund: ['/v2/transactions/:id', 'DELETE']
-}
+};
diff --git a/lib/transparent.js b/lib/transparent.js
index 01f6d93..63471a9 100644
--- a/lib/transparent.js
+++ b/lib/transparent.js
@@ -20,29 +20,29 @@
t.debug(TRANSACTION_URL);
t.debug('============================');
- this.billingInfoUrl = function(){return BILLING_INFO_URL};
- this.subscribeUrl = function(){return SUBSCRIBE_URL};
- this.transactionUrl = function(){return TRANSACTION_URL};
+ this.billingInfoUrl = function(){return BILLING_INFO_URL;};
+ this.subscribeUrl = function(){return SUBSCRIBE_URL;};
+ this.transactionUrl = function(){return TRANSACTION_URL;};
this.hidden_field = function(data){
return '';
- }
+ };
this.getResults = function(confirm, result, status, type, callback){
- validateQueryString(confirm, type, status, result)
+ validateQueryString(confirm, type, status, result);
t.request('/transparent/results/' + result, 'GET', callback);
- }
+ };
this.getFormValuesFromResult = function getFormValuesFromResult(result, type){
var fields = {};
var errors = [];
t.traverse(result.data,function(key, value, parent){
var shouldprint = false;
- var toprint = ''
+ var toprint = '';
if(value instanceof Object){
if(Object.keys(value).length === 0){
shouldprint = true;
- toprint = ''
+ toprint = '';
}
if(Object.hasOwnProperty('@') || Object.hasOwnProperty('#')){
shouldprint = true;
@@ -95,7 +95,7 @@
});
errors = handleFuzzyLogicSpecialCases(errors);
return {fields: fields, errors: errors};
- }
+ };
function processErrors(errors, parent){
var acc = [];
@@ -113,7 +113,7 @@
errors.forEach(function(item){
if(item instanceof Array){
//loop through the error list
- item.forEach(processSingleError)
+ item.forEach(processSingleError);
}
else{
//its a single error so grab it out
@@ -151,13 +151,13 @@
function makeDate(){
var d = new Date();
- var addleadingzero = function(n){ return (n<10)?'0'+n:''+n };
+ var addleadingzero = function(n){ return (n<10)?'0'+n:''+n ;};
return d.getUTCFullYear() + '-' +
- addleadingzero(d.getUTCMonth()+1) + '-' +
- addleadingzero(d.getUTCDate()) + 'T' +
- addleadingzero(d.getUTCHours()) + ':' +
- addleadingzero(d.getUTCMinutes()) + ':' +
- addleadingzero(d.getUTCSeconds()) + 'Z';
+ addleadingzero(d.getUTCMonth()+1) + '-' +
+ addleadingzero(d.getUTCDate()) + 'T' +
+ addleadingzero(d.getUTCHours()) + ':' +
+ addleadingzero(d.getUTCMinutes()) + ':' +
+ addleadingzero(d.getUTCSeconds()) + 'Z';
}
function hash(data) {
@@ -171,7 +171,7 @@
return hmac.digest('hex');
//php: 03021207ad681f2ea9b9e1fc20ac7ae460d8d988 <== Yes this sign is identical to the php version
//node: 03021207ad681f2ea9b9e1fc20ac7ae460d8d988
- }
+ }
function buildQueryStringFromSortedObject(params){
return params.map(function(p){
@@ -180,32 +180,32 @@
}
function makeSortedObject(obj, casesensitive){
- return Object.keys(obj).map(function(key){
- return {key: key, value: obj[key]};
- }).sort(function(a,b){
- return (casesensitive? a.key : a.key.toLowerCase()) > (casesensitive? b.key : b.key.toLowerCase());
- });
+ return Object.keys(obj).map(function(key){
+ return {key: key, value: obj[key]};
+ }).sort(function(a,b){
+ return (casesensitive? a.key : a.key.toLowerCase()) > (casesensitive? b.key : b.key.toLowerCase());
+ });
}
//Used for validating return params from Recurly
- function validateQueryString(confirm, type, status, result_key)
- {
- var values = {
+ function validateQueryString(confirm, type, status, result_key)
+ {
+ var values = {
result: result_key,
status: status,
type: type
- }
- var query_values = buildQueryStringFromSortedObject(makeSortedObject(values, true));
- hashed_values = hash(query_values);
+ };
+ var query_values = buildQueryStringFromSortedObject(makeSortedObject(values, true));
+ hashed_values = hash(query_values);
- if(hashed_values !== confirm) {
- throw "Error: Forged query string";
- }
+ if(hashed_values !== confirm) {
+ throw "Error: Forged query string";
+ }
return true;
- }
+ }
function handleFuzzyLogicSpecialCases(errors){
- var toreturn = []
+ var toreturn = [];
errors.forEach(function(e){
switch(e.field){
case 'billing_info[verification_value]':
@@ -228,6 +228,6 @@
return {field: name,reason: error.reason};
}
- }//END CLASS
+ }; //END CLASS
})();
\ No newline at end of file
diff --git a/lib/utils.js b/lib/utils.js
index 329e404..4834cdc 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,3 +1,6 @@
+var async = require('async'),
+ url = require('url');
+
exports.addParams = function(route, keys){
var newRoute = route.slice();
var path = newRoute[0];
@@ -6,9 +9,9 @@ exports.addParams = function(route, keys){
return keys[key];
});
return newRoute;
-}
+};
-exports.addQueryParams = function(route, params){
+var addQueryParams = exports.addQueryParams = function(route, params){
var newRoute = route.slice();
var _params = [];
if(params){
@@ -18,4 +21,101 @@ exports.addQueryParams = function(route, params){
}
if(_params.length > 0) return [newRoute[0] + '?' + _params.join('&'), newRoute[1]];
else return newRoute;
-}
\ No newline at end of file
+};
+
+var parseUrlLink = exports.parseUrlLink = function(result, callback) {
+ var links = {};
+ // Parse out our link
+ if(result.headers.link) {
+ // link format:
+ // '; rel="next", ; rel="last"'
+ result.headers.link.replace(/<([^>]*)>;\s*rel="([\w]*)\"/g, function(m, uri, type) {
+ links[type] = uri;
+ });
+ return links;
+ } else {
+ return null;
+ }
+};
+
+/**
+ * makeEachSeriesIterator
+ *
+ * Creates a function that will get all records for `route` and call `iterator` on each record.
+ *
+ * @param {Client} client Initialized recurly client.
+ * @param {String} route URL of resource
+ * @param {String} endpointName Plural name of resouce.
+ * @param {String} singleItemName Signular name of resource.
+ * @return {function(options, iterator, callback)}
+ */
+exports.makeEachSeriesIterator = function(client, route, endpointName, singleItemName) {
+ /**
+ *
+ * @param {Object} options like:
+ * per_page: {Number} Default 200. Max number of items to get per page. (Currently recurly api limits you to 200).
+ * limit: {Number} Default everything. Total number of items to get before calling the final callback.
+ * @param {function(item, callback)} iterator Function to call for each item returned from recurly. Call callback when
+ * you finish processing the item. Feeds items to iterator sequentially.
+ * @param {function(error)} callback Called when all items (up to limit) have been fetched and fed through `iterator`.
+ */
+ return function(options, iterator, callback) {
+
+ options = options || {};
+
+ var perPage = options.per_page || 200,
+ limit = options.limit,
+ initialCursor = addQueryParams(route, {per_page: perPage}),
+ count = 0;
+
+ nextPage(initialCursor);
+
+ function nextPage(cursor) {
+ client.request(cursor, function(error, result) {
+ if (error) {
+ return callback(error);
+ }
+
+ var records = result.data[endpointName][singleItemName];
+ if (!records) {
+ return callback(new Error('Response has no data'));
+ }
+
+ var links,
+ nextQueryString,
+ nextCursor,
+ isLimitHit;
+
+ // Parse out the next cursor
+ if (result.headers.link) {
+ links = parseUrlLink(result);
+ if (links.next) {
+ nextQueryString = url.parse(links.next, true).query;
+ nextCursor = addQueryParams(route, nextQueryString);
+ }
+ }
+
+ isLimitHit = limit && (count + records.length) > limit;
+ if (isLimitHit) {
+ // We will be over the limit, so slice down
+ records = records.slice(0, limit - count);
+ }
+
+ // Iterate through results
+ count += records.length;
+ async.eachSeries(records, iterator, function(error) {
+ if (error) {
+ return callback(error);
+ }
+
+ if (isLimitHit || !nextCursor) {
+ // Finished
+ callback();
+ } else {
+ nextPage(nextCursor);
+ }
+ });
+ });
+ }
+ };
+};
diff --git a/package.json b/package.json
index d869925..ad03cf1 100644
--- a/package.json
+++ b/package.json
@@ -1,21 +1,34 @@
-{ "name" : "node-recurly"
-, "description" : "Library for accessing the api for the Recurly recurring billing service."
-, "keywords" : [ "recurly", "e-commerce", "recurring billing" ]
-, "version" : "2.1.0"
-, "homepage" : "https://github.com/robrighter/node-recurly"
-, "author" : "Rob Righter (http://github.com/robrighter)"
-, "contributors" : [
- "Iván Guardado (http://github.com/IvanGuardado)",
- "Rob Righter (http://github.com/robrighter)",
- "Dmitriy Shekhovtsov (https://github.com/valorkin)"
- ]
-, "bugs" :
- { "web" : "https://github.com/robrighter/node-recurly/issues" }
-, "directories" : { "lib" : "./lib" }
-, "main" : "./lib/recurly.js"
-, "dependencies" : {
- "xml2js": ">= 0.4.0",
- "js2xml": "valorkin/js2xml"
-}
-, "engines" : { "node" : ">= 0.4" }
+{
+ "name": "node-recurly",
+ "description": "Library for accessing the api for the Recurly recurring billing service.",
+ "keywords": [
+ "recurly",
+ "e-commerce",
+ "recurring billing"
+ ],
+ "version": "3.0.0",
+ "homepage": "https://github.com/robrighter/node-recurly",
+ "author": "Rob Righter (http://github.com/robrighter)",
+ "contributors": [
+ "Iván Guardado (http://github.com/IvanGuardado)",
+ "Rob Righter (http://github.com/robrighter)",
+ "Dmitriy Shekhovtsov (https://github.com/valorkin)",
+ "Jeff Zabel (https://github.com/jzabel)"
+ ],
+ "bugs": {
+ "web": "https://github.com/robrighter/node-recurly/issues"
+ },
+ "directories": {
+ "lib": "./lib"
+ },
+ "main": "./lib/recurly.js",
+ "dependencies": {
+ "async": "^0.9.0",
+ "js2xml": "~1.0.4",
+ "lodash": "~2.4.1",
+ "xml2js": ">= 0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
}