UNPKG

freshbooks-js

Version:

Freshbooks for NodeJS ES Harmoney

375 lines (306 loc) 8.8 kB
/** filename: freshbooks.js author: grgory tomlinson copyright: 2014 gregory tomlinson https://github.com/gregory80/freshbooks-js An ES Harmony, ECMAScript 6, exploration of the Freshbooks API for NodeJS */ var freshbooks; (function(definition) { if (typeof exports == 'object' && typeof module == 'object') { // CommonJS/Node return definition(require, exports, module); } if (typeof define == 'function') { //AMD or Other return define.amd ? define(['require', 'exports'], definition) : define('freshbooks', definition); } definition(function() {}, freshbooks = {}); })(function(require, freshbooks) { "use strict"; // address short comings in node 0.11.12+ require("es6-shim"); var post,methods,method_proxy; // not all libraries are ecmascript 6 ready var thunkify = require('thunkify'); var Model = require("./Model").Model; var request = require('request'); // i/o OUTPUT for json=>XML var js2xmlparser = require("js2xmlparser"); // i/o INPUT for XML=>json // var xml2json = require('xml2json'); var oauth = require('oauth-sign'); var uuid = require('node-uuid'); post = thunkify(request.post); /** Endpoint defintions list http://developers.freshbooks.com/ */ methods = freshbooks.methods = [ /*** available callbacks: http://developers.freshbooks.com/docs/callbacks/#events Tips: http://developers.freshbooks.com/api-tips/ http://community.freshbooks.com/addons/submit regexp:subdomain = /(?:https?://)?([^.]+?)(?:[.](?:freshbooks|billingarm)[.]com)?/i */ "callback.create", "callback.verify", "callback.resendToken", "callback.list", "callback.delete", "category.create", "category.update", "category.get", "category.delete", "category.list", "client.create", "client.update", "client.get", "client.delete", "client.list", "contract.list", "estimate.create", "estimate.update", "estimate.get", "estimate.delete", "estimate.list", "estimate.sendByEmail", "expense.create", "expense.update", "expense.get", "expense.delete", "expense.list", "gateway.list", "invoice.create", "invoice.update", "invoice.get", "invoice.delete", "invoice.list", "invoice.sendByEmail", "invoice.sendBySnailMail", "invoice.lines.add", "invoice.lines.delete", "invoice.lines.update", "item.create", "item.update", "item.get", "item.delete", "item.list", "language.list", "payment.create", "payment.update", "payment.get", "payment.delete", "payment.list", "project.create", "project.update", "project.get", "project.delete", "project.list", "receipt.create", "receipt.update", "receipt.get", "receipt.delete", "recurring.create", "recurring.update", "recurring.get", "recurring.delete", "recurring.list", "recurring.lines.add", "recurring.lines.delete", "recurring.lines.update", "staff.current", "staff.get", "staff.list", "system.current", "task.create", "task.update", "task.get", "task.delete", "task.list", "tax.create", "tax.update", "tax.get", "tax.list", "tax.delete", "time_entry.create", "time_entry.update", "time_entry.get", "time_entry.delete", "time_entry.list" ]; /** Declare external methods */ methods.forEach(function(method_string_name,idx) { set_deep_value( freshbooks, method_string_name, remote_connection_wrapper(method_string_name, { basic_auth:false }) ); }); set_deep_value.call(this, freshbooks,"config.update", function(data) { data || (data={}); delete data.update; Object.assign(freshbooks.config, data); // return freshbooks; }); set_deep_value.call(this, freshbooks,"api.open",function(path, data, opts) { var args = Array.prototype.slice.call(arguments,1); data || (data={}); var fnc = get_deep_value(freshbooks,path); return fnc && typeof fnc === "function" && fnc.apply(freshbooks,args); }); function api_url(name) { return 'https://' + name + ".freshbooks.com/api/2.1/xml-in"; } function build_oauth_str(token, token_secret) { var oa = { oauth_token:token, oauth_consumer_key:freshbooks.config.oauth_consumer_key, oauth_signature_method:"PLAINTEXT", oauth_signature:freshbooks.config.oauth_consumer_secret + "&" + token_secret, oauth_nonce:uuid().replace(/-/g, ''), oauth_timestamp:Math.floor( Date.now() / 1000 ).toString(), oauth_version:"1.0" } return 'OAuth realm="",'+Object.keys(oa).map(function (i) { return i+'="'+oauth.rfc3986(oa[i])+'"' }).join(','); } // http://stackoverflow.com/questions/13719593/javascript-how-to-set-object-property-given-its-string-name // TODO, support lists correctly function set_deep_value(obj, path, value) { if (typeof path === "string") { var path = path.split('.'); } if(path.length > 1){ var p=path.shift(); if(obj[p]==null || typeof obj[p]!== 'object'){ obj[p] = {}; } set_deep_value(obj[p], path, value); } else { obj[path[0]] = value; } } function get_deep_value(obj, path) { if(typeof path === "string") { path = path.split("."); } var p; while( p = path.shift() ) { obj = obj[p] } return obj; } // for deprecated token connections (testing / development) function get_auth_data() { return { user:freshbooks.config.token, pass:"X" }; } method_proxy = Proxy.create({ get:function(proxy,name) { return { "@":{ method:name } } } }); // returns a generator function function remote_connection_wrapper(method_key, _options) { if(!method_key) { throw "Method name required"; } _options || (_options={}); return function *(data, opts) { data || (data={}); opts || (opts={}); var options = {}; [_options,opts].reduce( Object.assign, options ); var _url = api_url(options.subdomain); // console.log("connecting to _url", _url); var xml_wrap = {}, query_data_list, xml_req_body, data, res, body, noun, verb; /** When the verb is .create or .update Freshbooks API requires an additional parent node that is the noun for example, if method was category.create category:{ category_id:"value" } where as, in teh case of a .get that is not the case. category.get { category_id:"value" } .get, .current, .delete and .list do not share this behavior This code attempts normalize this; */ { let splitted = method_key.split("."); noun = splitted&&splitted[0]; verb = splitted&&splitted[1]; } query_data_list = [method_proxy[method_key]]; if(["create","update"].indexOf(verb) > -1) { { let xml_wrap = {}; xml_wrap[noun] = data; query_data_list.push(xml_wrap); } } else { query_data_list.push(data); } //http://www.2ality.com/2014/01/object-assign.html query_data_list.reduce(Object.assign, xml_wrap); xml_req_body = js2xmlparser("request", xml_wrap); /** Actually connect to remote server yield data, and pass to XML => JSON processor */ var params = { url:_url, body:xml_req_body, headers:{ 'User-Agent': 'freshbooks-js' } } // console.log("xml_req_body", xml_req_body,params); if(options.basic_auth) { params.auth = get_auth_data(); } else { // oauth processing // check oauth pieces // console.log("building oauth string") params.headers.Authorization = build_oauth_str(options.oauth_token, options.oauth_token_secret); } // console.log("Query using params", params); Object.assign(params.headers, options.headers||{}); data = yield post(params); res = data && data[0]; body = data && data[1]; return new Model({ method_key:method_key, noun:noun, verb:verb, xml:body, response:res, request_data:xml_req_body }); } } });