UNPKG

recurring

Version:

a recurly v2 api client for node.js

368 lines (310 loc) 10 kB
'use strict' const handleRecurlyError = require('./util').handleRecurlyError const RecurlyError = require('./recurly-error') const _ = require('lodash') const rparser = require('./parser.js') const debugR = require('debug')('recurring:request') const debug = require('debug')('recurring') const iterators = require('async-iterators') const pkg = require('../package.json') const querystring = require('querystring') const parser = rparser.createParser() class RecurlyData { constructor(options) { // Store a reference to the Recurring instance. this._recurring = options.recurring this.idField = options.idField this.enumerable = options.enumerable this.properties = { } this._resources = { } for (var i = 0; i < options.properties.length; i++) { RecurlyData.addProperty(this, options.properties[i]) } this.proplist = options.properties this.__defineGetter__('id', function() { return this.properties[options.idField] }) var idSetter = function() { var newval = arguments['0'] this.properties[options.idField] = newval this.href = this.constructor.ENDPOINT + '/' + newval } this.__defineSetter__('id', idSetter) this.__defineSetter__(options.idField, idSetter) } static get ENDPOINT() { return 'https://api.recurly.com/v2/' } static addProperty(instance, propname) { var getterFunc = function() { return this.properties[propname] } var setterFunc = function() { var newval = arguments['0'] this.properties[propname] = newval } instance.__defineGetter__(propname, getterFunc) instance.__defineSetter__(propname, setterFunc) } baseOptions() { return { headers: { 'Accept': 'application/xml', 'Authorization': this._recurring.AUTH_BASIC, 'User-Agent': `${pkg.name}/${pkg.version}`, 'X-Api-Version': '2.7' } } } go(method, uri, args, opts, callback) { if (typeof callback === 'undefined' && typeof opts === 'function') { callback = opts opts = { } } var options = _.merge(this.baseOptions(), opts) options.uri = uri if (method === 'GET') { options.qs = args } else { options.body = args } options.method = method this.execute(options, callback) } get(uri, args, opts, callback) { this.go('GET', uri, args, opts, callback) } put(uri, args, opts, callback) { this.go('PUT', uri, args, opts, callback) } post(uri, args, opts, callback) { this.go('POST', uri, args, opts, callback) } head(uri, args, opts, callback) { this.go('HEAD', uri, args, opts, callback) } all(filter, callback) { if (typeof callback === 'undefined' && typeof filter === 'function') { callback = filter filter = {} } filter = filter || {} if (!this.enumerable) { return callback(new Error(`${this.constructor.name} does not support .all()`)) } this.fetchAll(this.constructor.name, this.constructor.ENDPOINT, filter, (err, results) => { if (err) { return callback(err) } const data = { } _.each(results, item => { data[item[this.idField]] = item }) callback(null, data) }) } fetchAll(modelName, uri, filter, callback) { if (typeof callback === 'undefined' && typeof filter === 'function') { callback = filter filter = {} } filter = filter || {} var model = this._recurring[modelName]() var iterator = model.iterator(filter, uri) var result = [ ] iterators.forEachAsync(iterator, function(err, item, next) { if (err) { return callback(err) } result.push(item) return next() }, () => callback(null, result)) } fetch(callback) { if (!this.href) { throw (new Error('cannot fetch a record without an href')) } this.get(this.href, { }, (err, response, payload) => { if (err) { return callback(err) } if (response.statusCode === 404) { return callback(new Error('not_found')) } this.inflate(payload) callback(null, this) }) } destroy(callback) { const options = this.baseOptions() options.uri = this.href options.method = 'DELETE' const handleResponse = (err, response, payload) => { var error = handleRecurlyError(err, response, payload, [ 204 ]) if (error) { return callback(error) } this.deleted = true callback(null, this.deleted) } this._recurring.request(options, function(err, response, body) { if (body) { parser.parseXML(body, function(xmlerr, result) { if (xmlerr) { return handleResponse(err, response, body) } return handleResponse(err, response, result) }) } else { handleResponse(err, response) } }) } inflate(json) { if (typeof json !== 'object') { // TODO throw an error console.error(json) return } var keys = Object.keys(json) for (var i = 0; i < keys.length; i++) { var prop = keys[i] var value = json[prop] if (prop === 'a') { // Hackery. 'a' is a list of named anchors. We treat them specially. this.a = { } var anchors = Object.keys(value) for (var j = 0; j < anchors.length; j++) { this.a[value.name] = value } } // if (prop === 'a') { // // Hackery. 'a' is a list of named anchors. We treat them specially. // this.a = { } // var anchors = Object.keys(value) // for (var j = 0; j < anchors.length; j++) { // this.a[value[anchors[j]].name] = value[j] // } // } else if (value.hasOwnProperty('href') && (Object.keys(value).length === 1)) { if (!this._resources) { this._resources = { } } this._resources[prop] = value.href } else { this[prop] = value } } } execute(options, callback) { debug('execute called with options: %o', options) this._recurring.request(options, function(err, response, body) { debug('execute got response with status code: %s', _.get(response, 'statusCode')) debugR('execute got response with body: %O', body) if (err) { console.error('recurly.' + options.method, 'error ' + JSON.stringify(err)) return callback(err, response, { }) } if (response.statusCode === 404) { return parser.parseXML(body, function(err, result) { if (err) { console.error('recurly.get', 'xml parsing error:', JSON.stringify(err), 'from body:', body) return callback(err, response, { }) } var error = new RecurlyError(result) callback(error, response, { }) }) } if (response.statusCode === 401) { return callback(new Error('Your API key is missing or invalid'), response, {}) } if (options.noParse || response.statusCode === 204) { return callback(err, response, body) } parser.parseXML(body, function(err, result) { if (err) { console.error('recurly.get', 'xml parsing error:', JSON.stringify(err), 'from body:', body) return callback(err, response, { }) } callback(null, response, result) }) }) } iterator(filter, endpoint) { filter = filter || {} endpoint = endpoint || this.constructor.ENDPOINT let uri = filter ? `${endpoint}?${querystring.stringify(filter)}` : endpoint const result = [ ] let total = -1 let current = 0 return { next: function(cb) { // If we are already at the end, return nothing, if (total > -1 && current >= total) { return cb(null) } // If this is the first hit, fetch the total count, then the first set of results. if (total === -1) { return this.getTotalCount((err, res) => { if (err) { return cb(err) } total = res return this.getNextValue(cb) }) } // Otherwise, just return the next result. return this.getNextValue(cb) }, getTotalCount: done => { debug(`fetching result count.`) this.head(uri, null, { noParse: true }, (err, response, records) => { var error = handleRecurlyError(err, response, records, [ 200 ]) if (error) { return done(error) } total = parseInt(response.headers['x-records'], 10) debug(`result count: ${total}`) done(null, total) }) }, getNextValue: done => { // If we already have some results, return the next one. if (result.length) { current++ done(null, result.shift()) } // Otherwise fetch the next page of results if there are some. else { debug(`fetching iterator results: ${current}/${total}`) this.get(uri, { per_page: 200 }, (err, response, records) => { var error = handleRecurlyError(err, response, records, [ 200 ]) if (error) { return done(error) } // Ensure next attempt to fetch results pulls from next page. if (response.headers.link) { uri = response.headers.link.split('; rel="next"')[0].split(',').pop().trim().slice(1, -1) } // Process the results. _.each(records, record => { const item = this._recurring[this.constructor.name]() item.inflate(record) result.push(item) }) // Grab the first item. var item = result.shift() // Increment the current counter. current++ // Return it. done(err, item) }) } } } } } module.exports = RecurlyData