From 21ae8e101ddeb2c1f587908d84b8d467d65d9fa5 Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Thu, 8 Nov 2012 11:41:52 -0500 Subject: [PATCH 01/16] Added support for overriding access_token via params object --- index.js | 5 +++-- tools.js | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 tools.js diff --git a/index.js b/index.js index 80a41ae..59078eb 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,8 @@ var crypto = require('crypto'); var qs = require('querystring'); var restler = require('restler'); var util = require('util'); +var tools = require('./tools'); +var merge = tools.merge; var Faceplate = function(options) { @@ -117,8 +119,7 @@ var FaceplateSession = function(plate, signed_request) { params = {}; } - if (self.token) - params.access_token = self.token; + params = merge({access_token: self.token}, params || {}); try { restler.get('https://graph.facebook.com' + path, { query: params }).on('complete', function(data) { diff --git a/tools.js b/tools.js new file mode 100644 index 0000000..6caefb6 --- /dev/null +++ b/tools.js @@ -0,0 +1,12 @@ +function merge(obj1, obj2){ + for(x in obj2){ + if(obj2.hasOwnProperty(x)){ + obj1[x] = obj2[x]; + } + } +} + + +module.exports = { + 'merge': merge +}; \ No newline at end of file From efc87799ab674e79887c999b60352e5cf5004d93 Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Thu, 8 Nov 2012 11:42:49 -0500 Subject: [PATCH 02/16] Renamed FaceplateSession.post to FaceplateSession.postFeed --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 59078eb..d884220 100644 --- a/index.js +++ b/index.js @@ -158,7 +158,7 @@ var FaceplateSession = function(plate, signed_request) { restler.get('https://api.facebook.com/method/'+method, { query: params }).on('complete', onComplete); } - this.post = function (params, cb) { + this.postFeed = function (params, cb) { restler.post( 'https://graph.facebook.com/me/feed', { From 9c84aff8999645c62757770f082f90a21686286f Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Thu, 8 Nov 2012 11:59:53 -0500 Subject: [PATCH 03/16] Added FaceplateSession.post, changed FacePlateSession.postFeed to behave like other helper functions --- index.js | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index d884220..39a5134 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ var qs = require('querystring'); var restler = require('restler'); var util = require('util'); var tools = require('./tools'); +var fbError = require('./FacebookApiError'); var merge = tools.merge; var Faceplate = function(options) { @@ -157,21 +158,37 @@ var FaceplateSession = function(plate, signed_request) { } restler.get('https://api.facebook.com/method/'+method, { query: params }).on('complete', onComplete); } + this.post = function (url, params, cb) { + if(cb === undefined){ + cb = params; + params = {}; + } + // access_token can be passed via post data + params = merge({access_token: self.token}, params); + try{ + restler.post( + 'https://graph.facebook.com' + path, + { + data: params + }).on('complete', function (data) { + var result = JSON.parse(data); + cb(null, result.data ? result.data : result); + }); + } catch(err){ + cb(err, null); + } + } this.postFeed = function (params, cb) { - restler.post( - 'https://graph.facebook.com/me/feed', - { - query:{ - access_token:self.token - }, - data: params - }).on('complete', function (data) { - var result = JSON.parse(data); - cb(result.data ? result.data : result); + if(self.token){ + self.post('/me/feed', params, function(err, result){ + cb(result); }); + }else{ + cb(); + } } -} + module.exports.middleware = function(options) { return new Faceplate(options).middleware(); From c389f9280b17634f66923ab30d9c15badfa4da7f Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Thu, 8 Nov 2012 12:06:00 -0500 Subject: [PATCH 04/16] Added FacebookApiError type --- FacebookApiError.js | 16 ++++++++++++++++ index.js | 18 ++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 FacebookApiError.js diff --git a/FacebookApiError.js b/FacebookApiError.js new file mode 100644 index 0000000..a617352 --- /dev/null +++ b/FacebookApiError.js @@ -0,0 +1,16 @@ +var inherits = require('util').inherits; + +function FacebookApiError(error) { + Error.call(this); + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + for(x in error){ + if(error.hasOwnProperty(x)) + this[x] = error[x]; + } + +} + +inherits(FacebookApiError, Error); +module.exports = FacebookApiError; \ No newline at end of file diff --git a/index.js b/index.js index 39a5134..235564d 100644 --- a/index.js +++ b/index.js @@ -92,6 +92,14 @@ var FaceplateSession = function(plate, signed_request) { var self = this; + var _handleAPIResult(data, cb){ + var result = JSON.parse(data); + if(result.error){ + cb(new fbError(result.error), result); + }else{ + cb(null, result.data ? result.data : result); + } + } this.plate = plate; if (signed_request) { this.token = signed_request.access_token || signed_request.oauth_token; @@ -123,10 +131,7 @@ var FaceplateSession = function(plate, signed_request) { params = merge({access_token: self.token}, params || {}); try { - restler.get('https://graph.facebook.com' + path, { query: params }).on('complete', function(data) { - var result = JSON.parse(data); - cb(null, result); - }); + restler.get('https://graph.facebook.com' + path, { query: params }).on('complete', _handleAPIResult); } catch (err) { cb(err); } @@ -170,10 +175,7 @@ var FaceplateSession = function(plate, signed_request) { 'https://graph.facebook.com' + path, { data: params - }).on('complete', function (data) { - var result = JSON.parse(data); - cb(null, result.data ? result.data : result); - }); + }).on('complete', _handleAPIResult); } catch(err){ cb(err, null); } From b6183005137605f0e8e9887f3a5bc748931130c9 Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Thu, 8 Nov 2012 12:27:30 -0500 Subject: [PATCH 05/16] Added Faceplate.appSession --- index.js | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 235564d..0ce5f1c 100644 --- a/index.js +++ b/index.js @@ -92,20 +92,46 @@ var FaceplateSession = function(plate, signed_request) { var self = this; - var _handleAPIResult(data, cb){ + this.plate = plate; + if (signed_request) { + this.token = signed_request.access_token || signed_request.oauth_token; + this.signed_request = signed_request; + } + + function _handleAPIResult(data, cb){ var result = JSON.parse(data); if(result.error){ cb(new fbError(result.error), result); }else{ - cb(null, result.data ? result.data : result); + cb(null, result && result.data ? result.data : result); } } - this.plate = plate; - if (signed_request) { - this.token = signed_request.access_token || signed_request.oauth_token; - this.signed_request = signed_request; - } + this.appSession = function(cb){ + var params = { + client_id: self.plate.app_id, + client_secret: self.plate.secret, + grant_type: 'client_credentials' + }; + try{ + restler.post('https://graph.facebook.com/oauth/access_token',{ + data: params + }).on('complete', function(data){ + var result = qs.parse(data); + if(result.access_token){ + var session = new FaceplateSession(self.plate, self.signed_request); + session.token = result.access_token; + cb(null, session); + }else{ + result = JSON.parse(data); + cb(new fbError(result && result.error), result); + } + + }); + }catch(err){ + cb(err); + } + } this.app = function(cb) { self.get('/' + self.plate.app_id, function(err, app) { cb(app); From 6528ccc9f434e640b015f0635bddcd526b29044c Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Thu, 8 Nov 2012 12:30:59 -0500 Subject: [PATCH 06/16] Fixed typo --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 0ce5f1c..7ef8150 100644 --- a/index.js +++ b/index.js @@ -189,6 +189,7 @@ var FaceplateSession = function(plate, signed_request) { } restler.get('https://api.facebook.com/method/'+method, { query: params }).on('complete', onComplete); } + this.post = function (url, params, cb) { if(cb === undefined){ cb = params; @@ -216,8 +217,9 @@ var FaceplateSession = function(plate, signed_request) { cb(); } } - +} module.exports.middleware = function(options) { return new Faceplate(options).middleware(); } + From f1780e9cb183afa3cea39c4bb91083677a0951d6 Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Thu, 8 Nov 2012 12:36:32 -0500 Subject: [PATCH 07/16] Updated Readme --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 01724dc..9a248ae 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ var app = require('express').createServer( // show friends app.get('/friends', function(req, res) { - req.facebook.get('/me/friends', { limit: 4 }, function(friends) { + req.facebook.get('/me/friends', { limit: 4 }, function(err, friends) { res.send('friends: ' + require('util').inspect(friends)); }); }); // use fql to show my friends using this app app.get('/friends_using_app', function(req, res) { - req.facebook.fql('SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1', function(friends_using_app) { + req.facebook.fql('SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1', function(err, friends_using_app) { res.send('friends using app: ' + require('util').inspect(friends_using_app)); }); }); @@ -38,7 +38,7 @@ app.get('/multiquery', function(req, res) { likes: 'SELECT user_id, object_id, post_id FROM like WHERE user_id=me()', albums: 'SELECT object_id, cover_object_id, name FROM album WHERE owner=me()', }, - function(result) { + function(err, result) { var inspect = require('util').inspect; res.send('Yor likes: ' + inspect(result.likes) + ', your albums: ' + inspect(result.albums) ); }); @@ -49,6 +49,15 @@ app.get('/signed_request', function(req, res) { res.send('Signed Request details: ' + require('util').inspect(req.facebook.signed_request)); }); +app.get('/notify', function(req, res){ + // notifications require an app session + req.facebook.appSession(function(err, session){ + session.post('/1234/notifications', {href: '', template: 'Notification from app!'}, function(err, result){ + res.send("Sent notification to uid 1234!"); + }); + }); +}); + ``` ## License From 6d271ac00af8afb48fd1272709730b7442e0a601 Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Mon, 13 May 2013 14:16:00 -0400 Subject: [PATCH 08/16] initial commit --- .npmignore | 1 + README.md | 56 ++ index.js | 218 +++++++ node_modules/b64url/lib/b64url.js | 26 + node_modules/b64url/package.json | 28 + node_modules/b64url/readme.md | 10 + node_modules/restler/.npmignore | 3 + node_modules/restler/MIT-LICENSE | 20 + node_modules/restler/README.md | 210 +++++++ node_modules/restler/bin/restler | 23 + node_modules/restler/index.js | 1 + node_modules/restler/lib/multipartform.js | 203 +++++++ node_modules/restler/lib/restler.js | 488 ++++++++++++++++ node_modules/restler/package.json | 30 + node_modules/restler/test/all.js | 6 + node_modules/restler/test/restler.js | 672 ++++++++++++++++++++++ package.json | 25 + 17 files changed, 2020 insertions(+) create mode 100644 .npmignore create mode 100644 README.md create mode 100644 index.js create mode 100644 node_modules/b64url/lib/b64url.js create mode 100644 node_modules/b64url/package.json create mode 100644 node_modules/b64url/readme.md create mode 100644 node_modules/restler/.npmignore create mode 100644 node_modules/restler/MIT-LICENSE create mode 100644 node_modules/restler/README.md create mode 100755 node_modules/restler/bin/restler create mode 100644 node_modules/restler/index.js create mode 100644 node_modules/restler/lib/multipartform.js create mode 100644 node_modules/restler/lib/restler.js create mode 100644 node_modules/restler/package.json create mode 100644 node_modules/restler/test/all.js create mode 100644 node_modules/restler/test/restler.js create mode 100644 package.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..01724dc --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# faceplate + +A Node.js wrapper for Facebook authentication and API + +## Usage + +Use as a connect middleware + +```javascript +// create an express webserver +var app = require('express').createServer( + express.bodyParser(), + express.cookieParser(), + require('faceplate').middleware({ + app_id: process.env.FACEBOOK_APP_ID, + secret: process.env.FACEBOOK_SECRET, + scope: 'user_likes,user_photos,user_photo_video_tags' + }) +); + +// show friends +app.get('/friends', function(req, res) { + req.facebook.get('/me/friends', { limit: 4 }, function(friends) { + res.send('friends: ' + require('util').inspect(friends)); + }); +}); + +// use fql to show my friends using this app +app.get('/friends_using_app', function(req, res) { + req.facebook.fql('SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1', function(friends_using_app) { + res.send('friends using app: ' + require('util').inspect(friends_using_app)); + }); +}); + +// perform multiple fql queries at once +app.get('/multiquery', function(req, res) { + req.facebook.fql({ + likes: 'SELECT user_id, object_id, post_id FROM like WHERE user_id=me()', + albums: 'SELECT object_id, cover_object_id, name FROM album WHERE owner=me()', + }, + function(result) { + var inspect = require('util').inspect; + res.send('Yor likes: ' + inspect(result.likes) + ', your albums: ' + inspect(result.albums) ); + }); +}); + +// See the full signed request details +app.get('/signed_request', function(req, res) { + res.send('Signed Request details: ' + require('util').inspect(req.facebook.signed_request)); +}); + +``` + +## License + +MIT diff --git a/index.js b/index.js new file mode 100644 index 0000000..ba40526 --- /dev/null +++ b/index.js @@ -0,0 +1,218 @@ +var b64url = require('b64url'); +var crypto = require('crypto'); +var qs = require('querystring'); +var restler = require('restler'); + + +function safeParse(str){ +// console.log('parsing str: ', str) + if(typeof(str) != 'string') return str; + try{ return JSON.parse(str)} + catch(e){ + return {error: e, response: str} + } +} + +var Faceplate = function(options) { + + var self = this; + + this.options = options || {}; + this.app_id = this.options.app_id; + this.secret = this.options.secret; + + this.middleware = function() { + return function(req, res, next) { + + if (req.body.signed_request) { + self.parse_signed_request(req.body.signed_request, function(err, decoded_signed_request) { + req.facebook = new FaceplateSession(self, decoded_signed_request); + next(); + }); + } else if (req.cookies["fbsr_" + self.app_id]) { + console.log('in') + self.parse_signed_request(req.cookies["fbsr_" + self.app_id], function(err,decoded_signed_request){ + console.log('out') + req.facebook = new FaceplateSession(self, decoded_signed_request); + next(); + }); + } else { + req.facebook = new FaceplateSession(self); + next(); + } + }; + }; + + this.parse_signed_request = function(signed_request, cb) { + var encoded_data = signed_request.split('.', 2); + + var sig = encoded_data[0]; + var json = b64url.decode(encoded_data[1]); + var data = safeParse(json); + + // check algorithm + if (!data.algorithm || (data.algorithm.toUpperCase() != 'HMAC-SHA256')) { + cb(new Error("unknown algorithm. expected HMAC-SHA256")); + } + + // check signature + var secret = self.secret; + var expected_sig = crypto.createHmac('sha256', secret).update(encoded_data[1]).digest('base64').replace(/\+/g,'-').replace(/\//g,'_').replace('=',''); + + if (sig !== expected_sig) + cb(new Error("bad signature")); + + // not logged in or not authorized + if (!data.user_id) { + console.log('no user id') + cb(null,data); + return; + } + var user_id = data.user_id; + + if (data.access_token || data.oauth_token) { + console.log('has access token') + cb(null,data); + return; + } + + if (!data.code) + cb(new Error("no oauth token and no code to get one")); + + var params = { + client_id: self.app_id, + client_secret: self.secret, + redirect_uri: '', + code: data.code + }; + + var request = restler.get('https://graph.facebook.com/oauth/access_token', + { query:params }); + + console.log('sending request') + request.on('fail', function(data) { + console.log('failed..') + var result = safeParse(data); + result.user_id = user_id + cb(result); + }); + + request.on('success', function(data) { + console.log('win...', data) + data = qs.parse(data) + data.user_id = user_id + cb(null,data); + }); + }; +}; + +var FaceplateSession = function(plate, signed_request) { + + var self = this; + + this.plate = plate; + //console.log('request: ', signed_request) + if (signed_request) { + this.token = signed_request.access_token || signed_request.oauth_token; + this.signed_request = signed_request; + } + console.log('token: ', this.token) + + this.app = function(cb) { + self.get('/' + self.plate.app_id, function(err, app) { + cb(err,app); + }); + }; + + this.me = function(cb) { + if (self.token) { + self.get('/me', function(err, me) { + cb(err,me); + }); + } else { + cb(null,null); + } + }; + + this.get = function(path, params, cb) { + if (cb === undefined) { + cb = params; + params = {}; + } + + if (self.token) + params.access_token = self.token; + + try { + var request = restler.get('https://graph.facebook.com' + path, + { query: params }); + request.on('fail', function(data) { + console.log('fb fail ~> ', data) + var result = safeParse(data); + cb(result); + }); + request.on('success', function(data) { + var result = safeParse(data); + console.log('fb ~>', data) + cb(null, result); + }); + } catch (err) { + cb(err); + } + }; + + this.fql = function(query, cb) { + var params = { access_token: self.token, format:'json' }; + var method; + var onComplete; + + if (typeof query == 'string') { + method = 'fql.query'; + params.query = query; + onComplete = function(res){ + var result = safeParse(res); + cb(null, result.data ? result.data : result); + }; + } + else { + method = 'fql.multiquery'; + params.queries = JSON.stringify(query); + onComplete = function(res) { + if (res.error_code) + return cb(res); + + var data = {}; + res.forEach(function(q) { + data[q.name] = q.fql_result_set; + }); + cb(null,data); + }; + } + var request = restler.get('https://api.facebook.com/method/'+method, + { query: params }); + request.on('fail', function(data) { + var result = safeParse(data); + cb(result); + }); + request.on('success', onComplete); + }; + + this.post = function (params, cb) { + var request = restler.post( + 'https://graph.facebook.com/me/feed', + {query: {access_token: self.token}, data: params} + ); + request.on('fail', function(data) { + var result = safeParse(data); + cb(result); + }); + request.on('success', function (data) { + var result = safeParse(data); + cb(null, result.data ? result.data : result); + }); + }; +}; + +module.exports.middleware = function(options) { + return new Faceplate(options).middleware(); +}; diff --git a/node_modules/b64url/lib/b64url.js b/node_modules/b64url/lib/b64url.js new file mode 100644 index 0000000..6dbc81c --- /dev/null +++ b/node_modules/b64url/lib/b64url.js @@ -0,0 +1,26 @@ +function rtrim(data, chr) { + var drop = 0 + , len = data.length + while (data.charAt(len - 1 - drop) === chr) drop++ + return data.substr(0, len - drop) +} + +module.exports.safe = function(b64data) { + return rtrim(b64data, '=').replace(/\+/g, '-').replace(/\//g, '_') +} + +module.exports.encode = function(data) { + var buf = data + if (!(data instanceof Buffer)) { + buf = new Buffer(Buffer.byteLength(data)) + buf.write(data) + } + return exports.safe(buf.toString('base64')) +} + +module.exports.decode = function(data, encoding) { + encoding = encoding === undefined ? 'utf8' : encoding + var buf = new Buffer(data.replace(/-/g, '+').replace(/_/g, '/'), 'base64') + if (!encoding) return buf + return buf.toString(encoding) +} diff --git a/node_modules/b64url/package.json b/node_modules/b64url/package.json new file mode 100644 index 0000000..1eabe1f --- /dev/null +++ b/node_modules/b64url/package.json @@ -0,0 +1,28 @@ +{ + "name": "b64url", + "description": "URL safe base64 encoding/decoding.", + "version": "1.0.3", + "homepage": "https://github.com/nshah/nodejs-b64url", + "author": { + "name": "Naitik Shah", + "email": "n@daaku.org" + }, + "main": "lib/b64url", + "repository": { + "type": "git", + "url": "https://github.com/nshah/nodejs-b64url" + }, + "scripts": { + "test": "./node_modules/.bin/expresso -c" + }, + "devDependencies": { + "expresso": ">= 0.8.1" + }, + "engines": { + "node": ">= 0.4.1" + }, + "readme": "b64url\n======\n\nURL safe base64 encoding/decoding as described\n[here](http://tools.ietf.org/html/rfc3548#section-4).\n\n```javascript\nvar encoded = b64url.encode(data)\nvar decoded = b64url.decode(encoded)\n```\n", + "readmeFilename": "readme.md", + "_id": "b64url@1.0.3", + "_from": "b64url@1.0.3" +} diff --git a/node_modules/b64url/readme.md b/node_modules/b64url/readme.md new file mode 100644 index 0000000..5c38a83 --- /dev/null +++ b/node_modules/b64url/readme.md @@ -0,0 +1,10 @@ +b64url +====== + +URL safe base64 encoding/decoding as described +[here](http://tools.ietf.org/html/rfc3548#section-4). + +```javascript +var encoded = b64url.encode(data) +var decoded = b64url.decode(encoded) +``` diff --git a/node_modules/restler/.npmignore b/node_modules/restler/.npmignore new file mode 100644 index 0000000..32943a1 --- /dev/null +++ b/node_modules/restler/.npmignore @@ -0,0 +1,3 @@ +*.swp +.[Dd][Ss]_store +node_modules diff --git a/node_modules/restler/MIT-LICENSE b/node_modules/restler/MIT-LICENSE new file mode 100644 index 0000000..cc25ab4 --- /dev/null +++ b/node_modules/restler/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2009 Dan Webb + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/node_modules/restler/README.md b/node_modules/restler/README.md new file mode 100644 index 0000000..37ee3c6 --- /dev/null +++ b/node_modules/restler/README.md @@ -0,0 +1,210 @@ +Restler +======= + +(C) Dan Webb (dan@danwebb.net/@danwrong) 2011, Licensed under the MIT-LICENSE + +An HTTP client library for node.js (0.3 and up). Hides most of the complexity of creating and using http.Client. Very early days yet. + +**Release 2.x.x** will be dedicated to modifying how errors are handled and emitted. Currently errors are being fired as an on 'error' event but as [@ctavan](https://github.com/ctavan) pointed out on [issue #36](https://github.com/danwrong/restler/pull/36) a better approach (and more commonly in vogue now) would be to pass the error obj to the callback. + +Ths change will inevitably affect those using older < 0.2.x versions of restler. Those not ready to upgrade yet are encouraged to stay on the 0.2.x version. + +See [Version History](https://github.com/danwrong/restler/wiki/Version-History) for changes + + +Features +-------- + +* Easy interface for common operations via http.request +* Automatic serialization of post data +* Automatic serialization of query string data +* Automatic deserialization of XML, JSON and YAML responses to JavaScript objects (if you have js-yaml and/or xml2js in the require path) +* Provide your own deserialization functions for other datatypes +* Automatic following of redirects +* Send files with multipart requests +* Transparently handle SSL (just specify https in the URL) +* Deals with basic auth for you, just provide username and password options +* Simple service wrapper that allows you to easily put together REST API libraries +* Transparently handle content-encoded responses (gzip, deflate) (requires node 0.6+) +* Transparently handle different content charsets via [iconv](https://github.com/bnoordhuis/node-iconv) (if available) + + +API +--- + +### request(url, options) + +Basic method to make a request of any type. The function returns a RestRequest object that emits events: + +#### events + +* `complete: function(result, response)` - emitted when the request has finished whether it was successful or not. Gets passed the response result and the response object as arguments. If some error has occurred, `result` is always instance of `Error`, otherwise it contains response data. +* `success: function(data, response)` - emitted when the request was successful. Gets passed the response data and the response object as arguments. +* `fail: function(data, response)` - emitted when the request was successful, but 4xx status code returned. Gets passed the response data and the response object as arguments. +* `error: function(err, response)` - emitted when some errors have occurred (eg. connection aborted, parse, encoding, decoding failed or some other unhandled errors). Gets passed the `Error` object and the response object (when available) as arguments. +* `abort: function()` - emitted when `request.abort()` is called. +* `2XX`, `3XX`, `4XX`, `5XX: function(data, response)` - emitted for all requests with response codes in the range (eg. `2XX` emitted for 200, 201, 203). +* actual response code: function(data, response) - emitted for every single response code (eg. 404, 201, etc). + +#### members + +* `abort([error])` Cancels request. `abort` event is emitted. `request.aborted` is set to `true`. If non-falsy `error` is passed, then `error` will be additionaly emitted (with `error` passed as a param and `error.type` is set to `"abort"`). Otherwise only `complete` event will raise. +* `retry([timeout])` Re-sends request after `timeout` ms. Pending request is aborted. +* `aborted` Determines if request was aborted. + + +### get(url, options) + +Create a GET request. + +### post(url, options) + +Create a POST request. + +### put(url, options) + +Create a PUT request. + +### del(url, options) + +Create a DELETE request. + +### head(url, options) + +Create a HEAD request. + +### json(url, data, options) + +Send json `data` via GET method. + +### postJson(url, data, options) + +Send json `data` via POST method. + + +### Parsers + +You can give any of these to the parsers option to specify how the response data is deserialized. +In case of malformed content, parsers emit `error` event. Original data returned by server is stored in `response.raw`. + +#### parsers.auto + +Checks the content-type and then uses parsers.xml, parsers.json or parsers.yaml. +If the content type isn't recognised it just returns the data untouched. + +#### parsers.json, parsers.xml, parsers.yaml + +All of these attempt to turn the response into a JavaScript object. In order to use the YAML and XML parsers you must have yaml and/or xml2js installed. + +### Options + +* `method` Request method, can be get, post, put, del. Defaults to `"get"`. +* `query` Query string variables as a javascript object, will override the querystring in the URL. Defaults to empty. +* `data` The data to be added to the body of the request. Can be a string or any object. +Note that if you want your request body to be JSON with the `Content-Type: application/json`, you need to +`JSON.stringify` your object first. Otherwise, it will be sent as `application/x-www-form-urlencoded` and encoded accordingly. +Also you can use `json()` and `postJson()` methods. +* `parser` A function that will be called on the returned data. Use any of predefined `restler.parsers`. See parsers section below. Defaults to `restler.parsers.auto`. +* `encoding` The encoding of the request body. Defaults to `"utf8"`. +* `decoding` The encoding of the response body. For a list of supported values see [Buffers](http://nodejs.org/docs/latest/api/buffers.html#buffers). Additionally accepts `"buffer"` - returns response as `Buffer`. Defaults to `"utf8"`. +* `headers` A hash of HTTP headers to be sent. Defaults to `{ 'Accept': '*/*', 'User-Agent': 'Restler for node.js' }`. +* `username` Basic auth username. Defaults to empty. +* `password` Basic auth password. Defaults to empty. +* `multipart` If set the data passed will be formated as `multipart/form-encoded`. See multipart example below. Defaults to `false`. +* `client` A http.Client instance if you want to reuse or implement some kind of connection pooling. Defaults to empty. +* `followRedirects` If set will recursively follow redirects. Defaults to `true`. + + +Example usage +------------- + +```javascript +var sys = require('util'), + rest = require('./restler'); + +rest.get('http://google.com').on('complete', function(result) { + if (result instanceof Error) { + sys.puts('Error: ' + result.message); + this.retry(5000); // try again after 5 sec + } else { + sys.puts(result); + } +}); + +rest.get('http://twaud.io/api/v1/users/danwrong.json').on('complete', function(data) { + sys.puts(data[0].message); // auto convert to object +}); + +rest.get('http://twaud.io/api/v1/users/danwrong.xml').on('complete', function(data) { + sys.puts(data[0].sounds[0].sound[0].message); // auto convert to object +}); + +rest.post('http://user:pass@service.com/action', { + data: { id: 334 }, +}).on('complete', function(data, response) { + if (response.statusCode == 201) { + // you can get at the raw response like this... + } +}); + +// multipart request sending a file and using https +rest.post('https://twaud.io/api/v1/upload.json', { + multipart: true, + username: 'danwrong', + password: 'wouldntyouliketoknow', + data: { + 'sound[message]': 'hello from restler!', + 'sound[file]': rest.file('doug-e-fresh_the-show.mp3', null, null, null, 'audio/mpeg') + } +}).on('complete', function(data) { + sys.puts(data.audio_url); +}); + +// create a service constructor for very easy API wrappers a la HTTParty... +Twitter = rest.service(function(u, p) { + this.defaults.username = u; + this.defaults.password = p; +}, { + baseURL: 'http://twitter.com' +}, { + update: function(message) { + return this.post('/statuses/update.json', { data: { status: message } }); + } +}); + +var client = new Twitter('danwrong', 'password'); +client.update('Tweeting using a Restler service thingy').on('complete', function(data) { + sys.p(data); +}); + +// post JSON +var jsonData = { id: 334 }; +rest.postJson('http://example.com/action', jsonData).on('complete', function(data, response) { + // handle response +}); + +``` + +Running the tests +----------------- +install **[nodeunit](https://github.com/caolan/nodeunit)** + +```bash +npm install nodeunit +``` + +then + +```bash +node test/all.js +``` + +or + +```bash +nodeunit test/restler.js +``` + +TODO +---- +* What do you need? Let me know or fork. diff --git a/node_modules/restler/bin/restler b/node_modules/restler/bin/restler new file mode 100755 index 0000000..8f112de --- /dev/null +++ b/node_modules/restler/bin/restler @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +require.paths.push(process.cwd()); + +var sys = require('util'), + rest = require('../lib/restler'), + repl = require('repl'); + +var replServer = repl.start(); + +var exportMethods = { + sys: sys, + rest: rest +} + +Object.keys(exportMethods).forEach(function(exportMethod) { + replServer.context[exportMethod] = exportMethods[exportMethod]; +}); + +rest.get('http://twaud.io/api/v1/7.json').on('complete', function(data, response) { + console.log(response.headers); + replServer.context.data = data; +}); \ No newline at end of file diff --git a/node_modules/restler/index.js b/node_modules/restler/index.js new file mode 100644 index 0000000..9b280e8 --- /dev/null +++ b/node_modules/restler/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/restler'); \ No newline at end of file diff --git a/node_modules/restler/lib/multipartform.js b/node_modules/restler/lib/multipartform.js new file mode 100644 index 0000000..215d032 --- /dev/null +++ b/node_modules/restler/lib/multipartform.js @@ -0,0 +1,203 @@ +var fs = require('fs'); +var sys = require('util') +exports.defaultBoundary = '48940923NODERESLTER3890457293'; + + +// This little object allows us hijack the write method via duck-typing +// and write to strings or regular streams that support the write method. +function Stream(stream) { + //If the user pases a string for stream,we initalize one to write to + if (this._isString(stream)) { + this.string = ""; + } + this.stream = stream; + +} + +Stream.prototype = { + //write to an internal String or to the Stream + write: function(data) { + if (this.string != undefined) { + this.string += data; + } else { + this.stream.write(data, "binary"); + } + }, + + //stolen from underscore.js + _isString: function(obj) { + return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); + } +} + +function File(path, filename, fileSize, encoding, contentType) { + this.path = path; + this.filename = filename || this._basename(path); + this.fileSize = fileSize; + this.encoding = encoding || "binary"; + this.contentType = contentType || 'application/octet-stream'; +} + +File.prototype = { + _basename: function(path) { + var parts = path.split(/\/|\\/); + return parts[parts.length - 1]; + } +}; + +function Data(filename, contentType, data) { + this.filename = filename; + this.contentType = contentType || 'application/octet-stream'; + this.data = data; +} + +function Part(name, value, boundary) { + this.name = name; + this.value = value; + this.boundary = boundary; +} + + +Part.prototype = { + + //returns the Content-Disposition header + header: function() { + var header; + + if (this.value.data) { + header = "Content-Disposition: form-data; name=\"" + this.name + + "\"; filename=\"" + this.value.filename + "\"\r\n" + + "Content-Type: " + this.value.contentType; + } if (this.value instanceof File) { + header = "Content-Disposition: form-data; name=\"" + this.name + + "\"; filename=\"" + this.value.filename + "\"\r\n" + + "Content-Length: " + this.value.fileSize + "\r\n" + + "Content-Type: " + this.value.contentType; + } else { + header = "Content-Disposition: form-data; name=\"" + this.name + "\""; + } + + return "--" + this.boundary + "\r\n" + header + "\r\n\r\n"; + }, + + //calculates the size of the Part + sizeOf: function() { + var valueSize; + if (this.value instanceof File) { + valueSize = this.value.fileSize; + } else if (this.value.data) { + valueSize = this.value.data.length; + } else { + valueSize = this.value.length; + } + return valueSize + this.header().length + 2; + }, + + // Writes the Part out to a writable stream that supports the write(data) method + // You can also pass in a String and a String will be returned to the callback + // with the whole Part + // Calls the callback when complete + write: function(stream, callback) { + + var self = this; + + //first write the Content-Disposition + stream.write(this.header()); + + //Now write out the body of the Part + if (this.value instanceof File) { + fs.open(this.value.path, "r", 0666, function (err, fd) { + if (err) throw err; + + var position = 0; + + (function reader () { + fs.read(fd, 1024 * 4, position, "binary", function (er, chunk) { + if (er) callback(err); + stream.write(chunk); + position += 1024 * 4; + if (chunk) reader(); + else { + stream.write("\r\n") + callback(); + fs.close(fd); + } + }); + })(); // reader() + }); + } else { + stream.write(this.value + "\r\n"); + callback(); + } + } +} + +//Renamed to MultiPartRequest from Request +function MultiPartRequest(data, boundary) { + this.encoding = 'binary'; + this.boundary = boundary || exports.defaultBoundary; + this.data = data; + this.partNames = this._partNames(); +} + +MultiPartRequest.prototype = { + _partNames: function() { + var partNames = []; + for (var name in this.data) { + partNames.push(name) + } + return partNames; + }, + + write: function(stream, callback) { + var partCount = 0, self = this; + + // wrap the stream in our own Stream object + // See the Stream function above for the benefits of this + var stream = new Stream(stream); + + // Let each part write itself out to the stream + (function writePart() { + var partName = self.partNames[partCount]; + var part = new Part(partName, self.data[partName], self.boundary); + part.write(stream, function (err) { + if (err) { + callback(err); + return; + } + partCount += 1; + if (partCount < self.partNames.length) + writePart(); + else { + stream.write('--' + self.boundary + '--' + "\r\n"); + + if (callback) callback(stream.string || ""); + } + }); + })(); + } +} + +var exportMethods = { + file: function(path, filename, fileSize, encoding, contentType) { + return new File(path, filename, fileSize, encoding, contentType) + }, + data: function(filename, contentType, data) { + return new Data(filename, contentType, data); + }, + sizeOf: function(parts, boundary) { + var totalSize = 0; + boundary = boundary || exports.defaultBoundary; + for (var name in parts) totalSize += new Part(name, parts[name], boundary).sizeOf(); + return totalSize + boundary.length + 6; + }, + write: function(stream, data, callback, boundary) { + var r = new MultiPartRequest(data, boundary); + r.write(stream, callback); + return r; + } +} + +Object.keys(exportMethods).forEach(function(exportMethod) { + exports[exportMethod] = exportMethods[exportMethod] +}) diff --git a/node_modules/restler/lib/restler.js b/node_modules/restler/lib/restler.js new file mode 100644 index 0000000..7842164 --- /dev/null +++ b/node_modules/restler/lib/restler.js @@ -0,0 +1,488 @@ +var sys = require('util'); +var http = require('http'); +var https = require('https'); +var url = require('url'); +var qs = require('querystring'); +var multipart = require('./multipartform'); +var zlib = null; +var Iconv = null; + +try { + zlib = require('zlib'); +} catch (err) {} + +try { + Iconv = require('iconv').Iconv; +} catch (err) {} + +function mixin(target, source) { + source = source || {}; + Object.keys(source).forEach(function(key) { + target[key] = source[key]; + }); + + return target; +} + +function Request(uri, options) { + this.url = url.parse(uri); + this.options = options; + this.headers = { + 'Accept': '*/*', + 'User-Agent': 'Restler for node.js', + 'Host': this.url.host + }; + + if (zlib) { + this.headers['Accept-Encoding'] = 'gzip, deflate'; + } + + mixin(this.headers, options.headers || {}); + + // set port and method defaults + if (!this.url.port) this.url.port = (this.url.protocol == 'https:') ? '443' : '80'; + if (!this.options.method) this.options.method = (this.options.data) ? 'POST' : 'GET'; + if (typeof this.options.followRedirects == 'undefined') this.options.followRedirects = true; + + // stringify query given in options of not given in URL + if (this.options.query && !this.url.query) { + if (typeof this.options.query == 'object') + this.url.query = qs.stringify(this.options.query); + else this.url.query = this.options.query; + } + + this._applyBasicAuth(); + + if (this.options.multipart) { + this.headers['Content-Type'] = 'multipart/form-data; boundary=' + multipart.defaultBoundary; + } else { + if (typeof this.options.data == 'object') { + this.options.data = qs.stringify(this.options.data); + this.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + this.headers['Content-Length'] = this.options.data.length; + } + if(typeof this.options.data == 'string') { + var buffer = new Buffer(this.options.data, this.options.encoding || 'utf8'); + this.options.data = buffer; + this.headers['Content-Length'] = buffer.length; + } + } + + var proto = (this.url.protocol == 'https:') ? https : http; + + this.request = proto.request({ + host: this.url.hostname, + port: this.url.port, + path: this._fullPath(), + method: this.options.method, + headers: this.headers + }); + + this._makeRequest(); +} + +Request.prototype = new process.EventEmitter(); + +mixin(Request.prototype, { + _isRedirect: function(response) { + return ([301, 302, 303].indexOf(response.statusCode) >= 0); + }, + _fullPath: function() { + var path = this.url.pathname || '/'; + if (this.url.hash) path += this.url.hash; + if (this.url.query) path += '?' + this.url.query; + return path; + }, + _applyBasicAuth: function() { + var authParts; + + if (this.url.auth) { + authParts = this.url.auth.split(':'); + this.options.username = authParts[0]; + this.options.password = authParts[1]; + } + + if (this.options.username && this.options.password) { + var b = new Buffer([this.options.username, this.options.password].join(':')); + this.headers['Authorization'] = "Basic " + b.toString('base64'); + } + }, + _responseHandler: function(response) { + var self = this; + + if (self._isRedirect(response) && self.options.followRedirects) { + try { + self.url = url.parse(url.resolve(self.url.href, response.headers['location'])); + self._retry(); + // todo handle somehow infinite redirects + } catch(err) { + err.message = 'Failed to follow redirect: ' + err.message; + self._fireError(err, response); + } + } else { + var body = ''; + + response.setEncoding('binary'); + + response.on('data', function(chunk) { + body += chunk; + }); + + response.on('end', function() { + response.rawEncoded = body; + self._decode(new Buffer(body, 'binary'), response, function(err, body) { + if (err) { + self._fireError(err, response); + return; + } + response.raw = body; + body = self._iconv(body, response); + self._encode(body, response, function(err, body) { + if (err) { + self._fireError(err, response); + } else { + self._fireSuccess(body, response); + } + }); + }); + }); + } + }, + _decode: function(body, response, callback) { + var decoder = response.headers['content-encoding']; + if (decoder in decoders) { + decoders[decoder].call(response, body, callback); + } else { + callback(null, body); + } + }, + _iconv: function(body, response) { + if (Iconv) { + var charset = response.headers['content-type']; + if (charset) { + charset = /\bcharset=(.+)(?:;|$)/i.exec(charset); + if (charset) { + charset = charset[1].trim().toUpperCase(); + if (charset != 'UTF-8') { + try { + var iconv = new Iconv(charset, 'UTF-8//TRANSLIT//IGNORE'); + return iconv.convert(body); + } catch (err) {} + } + } + } + } + return body; + }, + _encode: function(body, response, callback) { + var self = this; + if (self.options.decoding == 'buffer') { + callback(null, body); + } else { + body = body.toString(self.options.decoding); + if (self.options.parser) { + self.options.parser.call(response, body, callback); + } else { + callback(null, body); + } + } + }, + _fireError: function(err, response) { + this.emit('error', err, response); + this.emit('complete', err, response); + }, + _fireSuccess: function(body, response) { + if (parseInt(response.statusCode) >= 400) { + this.emit('fail', body, response); + } else { + this.emit('success', body, response); + } + this.emit(response.statusCode.toString().replace(/\d{2}$/, 'XX'), body, response); + this.emit(response.statusCode.toString(), body, response); + this.emit('complete', body, response); + }, + _makeRequest: function() { + var self = this; + this.request.on('response', function(response) { + self._responseHandler(response); + }).on('error', function(err) { + if (!self.aborted) { + self._fireError(err, null); + } + }); + }, + _retry: function() { + this.request.removeAllListeners().on('error', function() {}); + if (this.request.finished) { + this.request.abort(); + } + Request.call(this, this.url.href, this.options); // reusing request object to handle recursive calls and remember listeners + this.run(); + }, + run: function() { + var self = this; + + if (this.options.multipart) { + multipart.write(this.request, this.options.data, function() { + self.request.end(); + }); + } else { + if (this.options.data) { + this.request.write(this.options.data.toString(), this.options.encoding || 'utf8'); + } + this.request.end(); + } + + return this; + }, + abort: function(err) { + var self = this; + + if (err) { + if (typeof err == 'string') { + err = new Error(err); + } else if (!(err instanceof Error)) { + err = new Error('AbortError'); + } + err.type = 'abort'; + } else { + err = null; + } + + self.request.on('close', function() { + if (err) { + self._fireError(err, null); + } else { + self.emit('complete', null, null); + } + }); + + self.aborted = true; + self.request.abort(); + self.emit('abort', err); + return this; + }, + retry: function(timeout) { + var self = this; + timeout = parseInt(timeout); + var fn = self._retry.bind(self); + if (!isFinite(timeout) || timeout <= 0) { + process.nextTick(fn, timeout); + } else { + setTimeout(fn, timeout); + } + return this; + } +}); + +function shortcutOptions(options, method) { + options = options || {}; + options.method = method; + options.parser = (typeof options.parser !== "undefined") ? options.parser : parsers.auto; + return options; +} + +function request(url, options) { + var request = new Request(url, options); + request.on('error', function() {}); + process.nextTick(request.run.bind(request)); + return request; +} + +function get(url, options) { + return request(url, shortcutOptions(options, 'GET')); +} + +function patch(url, options) { + return request(url, shortcutOptions(options, 'PATCH')); +} + +function post(url, options) { + return request(url, shortcutOptions(options, 'POST')); +} + +function put(url, options) { + return request(url, shortcutOptions(options, 'PUT')); +} + +function del(url, options) { + return request(url, shortcutOptions(options, 'DELETE')); +} + +function head(url, options) { + return request(url, shortcutOptions(options, 'HEAD')); +} + +function json(url, data, options, method) { + options = options || {}; + options.parser = (typeof options.parser !== "undefined") ? options.parser : parsers.auto; + options.headers = options.headers || {}; + options.headers['content-type'] = 'application/json'; + options.data = JSON.stringify(data); + options.method = method || 'GET'; + return request(url, options); +} + +function postJson(url, data, options) { + return json(url, data, options, 'POST'); +} + +var parsers = { + auto: function(data, callback) { + var contentType = this.headers['content-type']; + var contentParser; + if (contentType) { + contentType = contentType.replace(/;.+/, ''); // remove all except mime type (eg. text/html; charset=UTF-8) + if (contentType in parsers.auto.matchers) { + contentParser = parsers.auto.matchers[contentType]; + } else { + // custom (vendor) mime types + var parts = contentType.match(/^([\w-]+)\/vnd((?:\.(?:[\w-]+))+)\+([\w-]+)$/i); + if (parts) { + var type = parts[1]; + var vendors = parts[2].substr(1).split('.'); + var subtype = parts[3]; + var vendorType; + while (vendors.pop() && !(vendorType in parsers.auto.matchers)) { + vendorType = vendors.length + ? type + '/vnd.' + vendors.join('.') + '+' + subtype + : vendorType = type + '/' + subtype; + } + contentParser = parsers.auto.matchers[vendorType]; + } + } + } + if (typeof contentParser == 'function') { + contentParser.call(this, data, callback); + } else { + callback(null, data); + } + }, + json: function(data, callback) { + if (data && data.length) { + try { + callback(null, JSON.parse(data)); + } catch (err) { + err.message = 'Failed to parse JSON body: ' + err.message; + callback(err, null); + } + } else { + callback(null, null); + } + } +}; + +parsers.auto.matchers = { + 'application/json': parsers.json +}; + +try { + var yaml = require('yaml'); + + parsers.yaml = function(data, callback) { + if (data) { + try { + callback(null, yaml.eval(data)); + } catch (err) { + err.message = 'Failed to parse YAML body: ' + err.message; + callback(err, null); + } + } else { + callback(null, null); + } + }; + + parsers.auto.matchers['application/yaml'] = parsers.yaml; +} catch(e) {} + +try { + var xml2js = require('xml2js'); + + parsers.xml = function(data, callback) { + if (data) { + var parser = new xml2js.Parser(); + parser.parseString(data, function(err, data) { + if (err) { + err.message = 'Failed to parse XML body: ' + err.message; + } + callback(err, data); + }); + } else { + callback(null, null); + } + }; + + parsers.auto.matchers['application/xml'] = parsers.xml; +} catch(e) { } + +var decoders = { + gzip: function(buf, callback) { + zlib.gunzip(buf, callback); + }, + deflate: function(buf, callback) { + zlib.inflate(buf, callback); + } +}; + + +function Service(defaults) { + if (defaults.baseURL) { + this.baseURL = defaults.baseURL; + delete defaults.baseURL; + } + + this.defaults = defaults; +} + +mixin(Service.prototype, { + request: function(path, options) { + return request(this._url(path), this._withDefaults(options)); + }, + get: function(path, options) { + return get(this._url(path), this._withDefaults(options)); + }, + patch: function(path, options) { + return patch(this._url(path), this._withDefaults(options)); + }, + put: function(path, options) { + return put(this._url(path), this._withDefaults(options)); + }, + post: function(path, options) { + return post(this._url(path), this._withDefaults(options)); + }, + del: function(path, options) { + return del(this._url(path), this._withDefaults(options)); + }, + _url: function(path) { + if (this.baseURL) return url.resolve(this.baseURL, path); + else return path; + }, + _withDefaults: function(options) { + var o = mixin({}, this.defaults); + return mixin(o, options); + } +}); + +function service(constructor, defaults, methods) { + constructor.prototype = new Service(defaults || {}); + mixin(constructor.prototype, methods); + return constructor; +} + +mixin(exports, { + Request: Request, + Service: Service, + request: request, + service: service, + get: get, + patch: patch, + post: post, + put: put, + del: del, + head: head, + json: json, + postJson: postJson, + parsers: parsers, + file: multipart.file, + data: multipart.data +}); diff --git a/node_modules/restler/package.json b/node_modules/restler/package.json new file mode 100644 index 0000000..1dfde02 --- /dev/null +++ b/node_modules/restler/package.json @@ -0,0 +1,30 @@ +{ + "name": "restler", + "version": "2.0.0", + "description": "An HTTP client library for node.js", + "contributors": [ + { + "name": "Dan Webb", + "email": "dan@danwebb.net" + } + ], + "homepage": "https://github.com/danwrong/restler", + "directories": { + "lib": "./lib" + }, + "main": "./lib/restler", + "engines": { + "node": ">= 0.6.x" + }, + "dependencies": {}, + "devDependencies": { + "nodeunit": ">=0.5.0", + "xml2js": ">=0.1.0", + "yaml": ">=0.2.0", + "iconv": ">=1.0.0" + }, + "readme": "Restler\n=======\n\n(C) Dan Webb (dan@danwebb.net/@danwrong) 2011, Licensed under the MIT-LICENSE\n\nAn HTTP client library for node.js (0.3 and up). Hides most of the complexity of creating and using http.Client. Very early days yet.\n\n**Release 2.x.x** will be dedicated to modifying how errors are handled and emitted. Currently errors are being fired as an on 'error' event but as [@ctavan](https://github.com/ctavan) pointed out on [issue #36](https://github.com/danwrong/restler/pull/36) a better approach (and more commonly in vogue now) would be to pass the error obj to the callback.\n\nThs change will inevitably affect those using older < 0.2.x versions of restler. Those not ready to upgrade yet are encouraged to stay on the 0.2.x version.\n\nSee [Version History](https://github.com/danwrong/restler/wiki/Version-History) for changes\n\n\nFeatures\n--------\n\n* Easy interface for common operations via http.request\n* Automatic serialization of post data\n* Automatic serialization of query string data\n* Automatic deserialization of XML, JSON and YAML responses to JavaScript objects (if you have js-yaml and/or xml2js in the require path)\n* Provide your own deserialization functions for other datatypes\n* Automatic following of redirects\n* Send files with multipart requests\n* Transparently handle SSL (just specify https in the URL)\n* Deals with basic auth for you, just provide username and password options\n* Simple service wrapper that allows you to easily put together REST API libraries\n* Transparently handle content-encoded responses (gzip, deflate) (requires node 0.6+)\n* Transparently handle different content charsets via [iconv](https://github.com/bnoordhuis/node-iconv) (if available)\n\n\nAPI\n---\n\n### request(url, options)\n\nBasic method to make a request of any type. The function returns a RestRequest object that emits events:\n\n#### events\n\n* `complete: function(result, response)` - emitted when the request has finished whether it was successful or not. Gets passed the response result and the response object as arguments. If some error has occurred, `result` is always instance of `Error`, otherwise it contains response data.\n* `success: function(data, response)` - emitted when the request was successful. Gets passed the response data and the response object as arguments.\n* `fail: function(data, response)` - emitted when the request was successful, but 4xx status code returned. Gets passed the response data and the response object as arguments.\n* `error: function(err, response)` - emitted when some errors have occurred (eg. connection aborted, parse, encoding, decoding failed or some other unhandled errors). Gets passed the `Error` object and the response object (when available) as arguments.\n* `abort: function()` - emitted when `request.abort()` is called.\n* `2XX`, `3XX`, `4XX`, `5XX: function(data, response)` - emitted for all requests with response codes in the range (eg. `2XX` emitted for 200, 201, 203).\n* actual response code: function(data, response) - emitted for every single response code (eg. 404, 201, etc).\n\n#### members\n\n* `abort([error])` Cancels request. `abort` event is emitted. `request.aborted` is set to `true`. If non-falsy `error` is passed, then `error` will be additionaly emitted (with `error` passed as a param and `error.type` is set to `\"abort\"`). Otherwise only `complete` event will raise.\n* `retry([timeout])` Re-sends request after `timeout` ms. Pending request is aborted.\n* `aborted` Determines if request was aborted.\n\n\n### get(url, options)\n\nCreate a GET request.\n\n### post(url, options)\n\nCreate a POST request.\n\n### put(url, options)\n\nCreate a PUT request.\n\n### del(url, options)\n\nCreate a DELETE request.\n\n### head(url, options)\n\nCreate a HEAD request.\n\n### json(url, data, options)\n\nSend json `data` via GET method.\n\n### postJson(url, data, options)\n\nSend json `data` via POST method.\n\n\n### Parsers\n\nYou can give any of these to the parsers option to specify how the response data is deserialized.\nIn case of malformed content, parsers emit `error` event. Original data returned by server is stored in `response.raw`.\n\n#### parsers.auto\n\nChecks the content-type and then uses parsers.xml, parsers.json or parsers.yaml.\nIf the content type isn't recognised it just returns the data untouched.\n\n#### parsers.json, parsers.xml, parsers.yaml\n\nAll of these attempt to turn the response into a JavaScript object. In order to use the YAML and XML parsers you must have yaml and/or xml2js installed.\n\n### Options\n\n* `method` Request method, can be get, post, put, del. Defaults to `\"get\"`.\n* `query` Query string variables as a javascript object, will override the querystring in the URL. Defaults to empty.\n* `data` The data to be added to the body of the request. Can be a string or any object.\nNote that if you want your request body to be JSON with the `Content-Type: application/json`, you need to\n`JSON.stringify` your object first. Otherwise, it will be sent as `application/x-www-form-urlencoded` and encoded accordingly.\nAlso you can use `json()` and `postJson()` methods.\n* `parser` A function that will be called on the returned data. Use any of predefined `restler.parsers`. See parsers section below. Defaults to `restler.parsers.auto`.\n* `encoding` The encoding of the request body. Defaults to `\"utf8\"`.\n* `decoding` The encoding of the response body. For a list of supported values see [Buffers](http://nodejs.org/docs/latest/api/buffers.html#buffers). Additionally accepts `\"buffer\"` - returns response as `Buffer`. Defaults to `\"utf8\"`.\n* `headers` A hash of HTTP headers to be sent. Defaults to `{ 'Accept': '*/*', 'User-Agent': 'Restler for node.js' }`.\n* `username` Basic auth username. Defaults to empty.\n* `password` Basic auth password. Defaults to empty.\n* `multipart` If set the data passed will be formated as `multipart/form-encoded`. See multipart example below. Defaults to `false`.\n* `client` A http.Client instance if you want to reuse or implement some kind of connection pooling. Defaults to empty.\n* `followRedirects` If set will recursively follow redirects. Defaults to `true`.\n\n\nExample usage\n-------------\n\n```javascript\nvar sys = require('util'),\n rest = require('./restler');\n\nrest.get('http://google.com').on('complete', function(result) {\n if (result instanceof Error) {\n sys.puts('Error: ' + result.message);\n this.retry(5000); // try again after 5 sec\n } else {\n sys.puts(result);\n }\n});\n\nrest.get('http://twaud.io/api/v1/users/danwrong.json').on('complete', function(data) {\n sys.puts(data[0].message); // auto convert to object\n});\n\nrest.get('http://twaud.io/api/v1/users/danwrong.xml').on('complete', function(data) {\n sys.puts(data[0].sounds[0].sound[0].message); // auto convert to object\n});\n\nrest.post('http://user:pass@service.com/action', {\n data: { id: 334 },\n}).on('complete', function(data, response) {\n if (response.statusCode == 201) {\n // you can get at the raw response like this...\n }\n});\n\n// multipart request sending a file and using https\nrest.post('https://twaud.io/api/v1/upload.json', {\n multipart: true,\n username: 'danwrong',\n password: 'wouldntyouliketoknow',\n data: {\n 'sound[message]': 'hello from restler!',\n 'sound[file]': rest.file('doug-e-fresh_the-show.mp3', null, null, null, 'audio/mpeg')\n }\n}).on('complete', function(data) {\n sys.puts(data.audio_url);\n});\n\n// create a service constructor for very easy API wrappers a la HTTParty...\nTwitter = rest.service(function(u, p) {\n this.defaults.username = u;\n this.defaults.password = p;\n}, {\n baseURL: 'http://twitter.com'\n}, {\n update: function(message) {\n return this.post('/statuses/update.json', { data: { status: message } });\n }\n});\n\nvar client = new Twitter('danwrong', 'password');\nclient.update('Tweeting using a Restler service thingy').on('complete', function(data) {\n sys.p(data);\n});\n\n// post JSON\nvar jsonData = { id: 334 };\nrest.postJson('http://example.com/action', jsonData).on('complete', function(data, response) {\n // handle response\n});\n\n```\n\nRunning the tests\n-----------------\ninstall **[nodeunit](https://github.com/caolan/nodeunit)**\n\n```bash\nnpm install nodeunit\n```\n\nthen\n\n```bash\nnode test/all.js\n```\n\nor\n\n```bash\nnodeunit test/restler.js\n```\n\nTODO\n----\n* What do you need? Let me know or fork.\n", + "readmeFilename": "README.md", + "_id": "restler@2.0.0", + "_from": "restler@2.0.0" +} diff --git a/node_modules/restler/test/all.js b/node_modules/restler/test/all.js new file mode 100644 index 0000000..12f3044 --- /dev/null +++ b/node_modules/restler/test/all.js @@ -0,0 +1,6 @@ + +require('./restler'); // debug +var nodeunit = require('nodeunit'); +var reporter = nodeunit.reporters['default']; +process.chdir(__dirname); +reporter.run(['restler.js']); diff --git a/node_modules/restler/test/restler.js b/node_modules/restler/test/restler.js new file mode 100644 index 0000000..1165f9b --- /dev/null +++ b/node_modules/restler/test/restler.js @@ -0,0 +1,672 @@ + +var rest = require('../lib/restler'); +var http = require('http'); +var sys = require('util'); +var path = require('path'); +var fs = require('fs'); +var crypto = require('crypto'); +var zlib = null; +var Iconv = null; + +try { + zlib = require('zlib'); +} catch (err) {} + +try { + Iconv = require('iconv').Iconv; +} catch (err) {} + +var p = sys.inspect; + +var port = 9000; +var hostname = 'localhost'; +var host = 'http://' + hostname + ':' + port; + +var nodeunit = require('nodeunit'); +nodeunit.assert.re = function(actual, expected, message) { + new RegExp(expected).test(actual) || nodeunit.assert.fail(actual, expected, message, '~=', nodeunit.assert.re); +}; + + +function setup(response) { + return function(next) { + this.server = http.createServer(response); + this.server.listen(port, hostname, next); + }; +} + +function teardown() { + return function (next) { + this.server._handle && this.server.close(); + process.nextTick(next); + }; +} + + +function echoResponse(request, response) { + if (request.headers['x-connection-abort'] == 'true') { + request.connection.destroy(); + return; + } + var echo = []; + echo.push(request.method + ' ' + request.url + ' HTTP/' + request.httpVersion); + for (var header in request.headers) { + echo.push(header + ': ' + request.headers[header]); + } + echo.push('', ''); + echo = echo.join('\r\n'); + + request.addListener('data', function(chunk) { + echo += chunk.toString('binary'); + }); + + request.addListener('end', function() { + response.writeHead(request.headers['x-status-code'] || 200, { + 'content-type': 'text/plain', + 'content-length': echo.length, + 'request-method': request.method.toLowerCase() + }); + setTimeout(function() { + response.end(request.method == 'HEAD' ? undefined : echo); + }, request.headers['x-delay'] | 0); + }); +} + +module.exports['Basic'] = { + + setUp: setup(echoResponse), + tearDown: teardown(), + + 'Should GET': function (test) { + rest.get(host).on('complete', function(data) { + test.re(data, /^GET/, 'should be GET'); + test.done(); + }); + }, + + 'Should PATCH': function(test) { + rest.patch(host).on('complete', function(data) { + test.re(data, /^PATCH/, 'should be PATCH'); + test.done(); + }); + }, + + 'Should PUT': function(test) { + rest.put(host).on('complete', function(data) { + test.re(data, /^PUT/, 'should be PUT'); + test.done(); + }); + }, + + 'Should POST': function(test) { + rest.post(host).on('complete', function(data) { + test.re(data, /^POST/, 'should be POST'); + test.done(); + }); + }, + + 'Should DELETE': function(test) { + rest.del(host).on('complete', function(data) { + test.re(data, /^DELETE/, 'should be DELETE'); + test.done(); + }); + }, + + 'Should HEAD': function(test) { + rest.head(host).on('complete', function(data, response) { + test.equal(response.headers['request-method'], 'head', 'should be HEAD'); + test.done(); + }); + }, + + 'Should GET withouth path': function(test) { + rest.get(host).on('complete', function(data) { + test.re(data, /^GET \//, 'should hit /'); + test.done(); + }); + }, + + 'Should GET path': function(test) { + rest.get(host + '/thing').on('complete', function(data) { + test.re(data, /^GET \/thing/, 'should hit /thing'); + test.done(); + }); + }, + + 'Should preserve query string in url': function(test) { + rest.get(host + '/thing?boo=yah').on('complete', function(data) { + test.re(data, /^GET \/thing\?boo\=yah/, 'should hit /thing?boo=yah'); + test.done(); + }); + }, + + 'Should serialize query': function(test) { + rest.get(host, { query: { q: 'balls' } }).on('complete', function(data) { + test.re(data, /^GET \/\?q\=balls/, 'should hit /?q=balls'); + test.done(); + }); + }, + + 'Should POST body': function(test) { + rest.post(host, { data: 'balls' }).on('complete', function(data) { + test.re(data, /\r\n\r\nballs/, 'should have balls in the body'); + test.done(); + }); + }, + + 'Should serialize POST body': function(test) { + rest.post(host, { data: { q: 'balls' } }).on('complete', function(data) { + test.re(data, /content-type\: application\/x-www-form-urlencoded/, 'should set content-type'); + test.re(data, /content-length\: 7/, 'should set content-length'); + test.re(data, /\r\n\r\nq=balls/, 'should have balls in the body'); + test.done(); + }); + }, + + 'Should send headers': function(test) { + rest.get(host, { + headers: { 'Content-Type': 'application/json' } + }).on('complete', function(data) { + test.re(data, /content\-type\: application\/json/, 'should have "content-type" header'); + test.done(); + }); + }, + + 'Should send basic auth': function(test) { + rest.post(host, { username: 'danwrong', password: 'flange' }).on('complete', function(data) { + test.re(data, /authorization\: Basic ZGFud3Jvbmc6Zmxhbmdl/, 'should have "authorization "header'); + test.done(); + }); + }, + + 'Should send basic auth if in url': function(test) { + rest.post('http://danwrong:flange@' + hostname + ':' + port).on('complete', function(data) { + test.re(data, /authorization\: Basic ZGFud3Jvbmc6Zmxhbmdl/, 'should have "authorization" header'); + test.done(); + }); + }, + + 'Should fire 2XX and 200 events': function(test) { + test.expect(3); + rest.get(host).on('2XX', function() { + test.ok(true); + }).on('200', function() { + test.ok(true); + }).on('complete', function() { + test.ok(true); + test.done(); + }); + }, + + 'Should fire fail, 4XX and 404 events for 404': function(test) { + test.expect(4); + rest.get(host, { headers: { 'x-status-code': 404 }}).on('fail', function() { + test.ok(true); + }).on('4XX', function() { + test.ok(true); + }).on('404', function() { + test.ok(true); + }).on('complete', function() { + test.ok(true); + test.done(); + }); + }, + + 'Should fire error and complete events on connection abort': function(test) { + test.expect(2); + rest.get(host, { headers: { 'x-connection-abort': 'true' }}).on('error', function() { + test.ok(true); + }).on('complete', function() { + test.ok(true); + test.done(); + }); + }, + + 'Should correctly retry': function(test) { + var counter = 0; + rest.get(host, { headers: { 'x-connection-abort': 'true' }}).on('complete', function() { + if (++counter < 3) { + this.retry(10); + } else { + test.ok(true); + test.done(); + } + }); + }, + + 'Should correctly retry after abort': function(test) { + var counter = 0; + rest.get(host).on('complete', function() { + if (++counter < 3) { + this.retry().abort(); + } else { + test.ok(true); + test.done(); + } + }).abort(); + }, + + 'Should correctly retry while pending': function(test) { + var counter = 0, request; + function command() { + var args = [].slice.call(arguments); + var method = args.shift(); + method && setTimeout(function() { + request[method](); + command.apply(null, args); + }, 50); + } + + request = rest.get(host, { headers: { 'x-delay': '1000' } }).on('complete', function() { + if (++counter < 3) { + command('retry', 'abort'); + } else { + test.ok(true); + test.done(); + } + }); + command('abort'); + } + +}; + +module.exports['Multipart'] = { + + setUp: setup(echoResponse), + tearDown: teardown(), + + 'Test multipart request with simple vars': function(test) { + rest.post(host, { + data: { a: 1, b: 'thing' }, + multipart: true + }).on('complete', function(data) { + test.re(data, /content-type\: multipart\/form-data/, 'should set "content-type" header') + test.re(data, /name="a"(\s)+1/, 'should send a=1'); + test.re(data, /name="b"(\s)+thing/, 'should send b=thing'); + test.done(); + }); + } + +}; + + +function dataResponse(request, response) { + switch (request.url) { + case '/json': + response.writeHead(200, { 'content-type': 'application/json' }); + response.end('{ "ok": true }'); + break; + case '/xml': + response.writeHead(200, { 'content-type': 'application/xml' }); + response.end('true'); + break; + case '/yaml': + response.writeHead(200, { 'content-type': 'application/yaml' }); + response.end('ok: true'); + break; + case '/gzip': + response.writeHead(200, { 'content-encoding': 'gzip' }); + response.end(Buffer('H4sIAAAAAAAAA0vOzy0oSi0uTk1RSEksSUweHFwADdgOgJYAAAA=', 'base64')); + break; + case '/deflate': + response.writeHead(200, { 'content-encoding': 'deflate' }); + response.end(Buffer('eJxLzs8tKEotLk5NUUhJLElMHhxcAI9GO1c=', 'base64')); + break; + case '/truth': + response.writeHead(200, { + 'content-encoding': 'deflate', + 'content-type': 'application/json' + }); + response.end(Buffer('eJw1i0sKgDAQQ++S9Sj+cDFXEReCoy2UCv0oIt7dEZEsEvKSC4eZErjoCTaCr5uQjICHilQjYfLxkAD+g/IN3BCcXXT3GSF7u0uI2vjs3HubwW1ZdwRRcCZj/QpOIcv9ACXbJLo=', 'base64')); + break; + case '/binary': + response.writeHead(200); + response.end(Buffer([9, 30, 64, 135, 200])); + break; + case '/push-json': + var echo = ''; + request.addListener('data', function(chunk) { + echo += chunk.toString('binary'); + }); + request.addListener('end', function() { + response.writeHead(200, { + 'content-type': 'application/json' + }); + response.end(JSON.stringify(JSON.parse(echo))); + }); + break; + case '/custom-mime': + response.writeHead(200, { + 'content-type': 'application/vnd.github.beta.raw+json; charset=UTF-8' + }); + response.end(JSON.stringify([6,6,6])); + break; + case '/mal-json': + response.writeHead(200, { 'content-type': 'application/json' }); + response.end('Чебурашка'); + break; + case '/mal-xml': + response.writeHead(200, { 'content-type': 'application/xml' }); + response.end('Чебурашка'); + break; + case '/mal-yaml': + response.writeHead(200, { 'content-type': 'application/yaml' }); + response.end('{Чебурашка'); + break; + case '/abort': + setTimeout(function() { + response.writeHead(200); + response.end('not aborted'); + }, 100); + break; + case '/charset': + response.writeHead(200, { + 'content-type': 'text/plain; charset=windows-1251' + }); + response.end(Buffer('e0e1e2e3e4e5b8e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', 'hex')); + break; + default: + response.writeHead(404); + response.end(); + } +} + +module.exports['Deserialization'] = { + + setUp: setup(dataResponse), + tearDown: teardown(), + + 'Should parse JSON': function(test) { + rest.get(host + '/json').on('complete', function(data) { + test.equal(data.ok, true, 'returned: ' + p(data)); + test.done(); + }); + }, + + 'Should parse XML': function(test) { + rest.get(host + '/xml').on('complete', function(data, response) { + test.equal(data.ok, 'true', 'returned: ' + response.raw + ' || ' + p(data)); + test.done(); + }); + }, + + 'Should parse YAML': function(test) { + rest.get(host + '/yaml').on('complete', function(data) { + test.equal(data.ok, true, 'returned: ' + p(data)); + test.done(); + }); + }, + + 'Should gunzip': function(test) { + if (zlib) { + rest.get(host + '/gzip').on('complete', function(data) { + test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data)); + test.done(); + }); + } else { + test.done(); + } + }, + + 'Should inflate': function(test) { + if (zlib) { + rest.get(host + '/deflate').on('complete', function(data) { + test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data)); + test.done(); + }) + } else { + test.done(); + } + }, + + 'Should decode and parse': function(test) { + if (zlib) { + rest.get(host + '/truth').on('complete', function(data) { + try { + with (data) { + var result = what + (is + the + answer + to + life + the + universe + and + everything).length; + } + test.equal(result, 42, 'returned: ' + p(data)); + } catch (err) { + test.ok(false, 'returned: ' + p(data)); + } + test.done(); + }) + } else { + test.done(); + } + }, + + 'Should decode as buffer': function(test) { + rest.get(host + '/binary', { decoding: 'buffer' }).on('complete', function(data) { + test.ok(data instanceof Buffer, 'should be buffer'); + test.equal(data.toString('base64'), 'CR5Ah8g=', 'returned: ' + p(data)); + test.done(); + }) + }, + + 'Should decode as binary': function(test) { + rest.get(host + '/binary', { decoding: 'binary' }).on('complete', function(data) { + test.ok(typeof data == 'string', 'should be string: ' + p(data)); + test.equal(data, '\t\u001e@‡È', 'returned: ' + p(data)); + test.done(); + }) + }, + + 'Should decode as base64': function(test) { + rest.get(host + '/binary', { decoding: 'base64' }).on('complete', function(data) { + test.ok(typeof data == 'string', 'should be string: ' + p(data)); + test.equal(data, 'CR5Ah8g=', 'returned: ' + p(data)); + test.done(); + }) + }, + + 'Should post and parse JSON': function(test) { + var obj = { secret : 'very secret string' }; + rest.post(host + '/push-json', { + headers: { + 'content-type': 'application/json' + }, + data: JSON.stringify(obj) + }).on('complete', function(data) { + test.equal(obj.secret, data.secret, 'returned: ' + p(data)); + test.done(); + }) + }, + + 'Should post and parse JSON via shortcut method': function(test) { + var obj = { secret : 'very secret string' }; + rest.postJson(host + '/push-json', obj).on('complete', function(data) { + test.equal(obj.secret, data.secret, 'returned: ' + p(data)); + test.done(); + }); + }, + + 'Should understand custom mime-type': function(test) { + rest.parsers.auto.matchers['application/vnd.github+json'] = function(data, callback) { + rest.parsers.json.call(this, data, function(err, data) { + err || (data.__parsedBy__ = 'github'); + callback(err, data); + }); + }; + rest.get(host + '/custom-mime').on('complete', function(data) { + test.expect(3); + test.ok(Array.isArray(data), 'should be array, returned: ' + p(data)); + test.equal(data.join(''), '666', 'should be [6,6,6], returned: ' + p(data)); + test.equal(data.__parsedBy__, 'github', 'should use vendor-specific parser, returned: ' + p(data.__parsedBy__)); + test.done(); + }); + }, + + 'Should correctly soft-abort request': function(test) { + test.expect(4); + rest.get(host + '/abort').on('complete', function(data) { + test.equal(data, null, 'data should be null'); + test.equal(this.aborted, true, 'should be aborted'); + test.done(); + }).on('error', function(err) { + test.ok(false, 'should not emit error event'); + }).on('abort', function(err) { + test.equal(err, null, 'err should be null'); + test.equal(this.aborted, true, 'should be aborted'); + }).on('success', function() { + test.ok(false, 'should not emit success event'); + }).on('fail', function() { + test.ok(false, 'should not emit fail event'); + }).abort(); + }, + + 'Should correctly hard-abort request': function(test) { + test.expect(4); + rest.get(host + '/abort').on('complete', function(data) { + test.ok(data instanceof Error, 'should be error, got: ' + p(data)); + test.equal(this.aborted, true, 'should be aborted'); + test.done(); + }).on('error', function(err) { + test.ok(err instanceof Error, 'should be error, got: ' + p(err)); + }).on('abort', function(err) { + test.equal(this.aborted, true, 'should be aborted'); + }).on('success', function() { + test.ok(false, 'should not emit success event'); + }).on('fail', function() { + test.ok(false, 'should not emit fail event'); + }).abort(true); + }, + + 'Should correctly handle malformed JSON': function(test) { + test.expect(4); + rest.get(host + '/mal-json').on('complete', function(data, response) { + test.ok(data instanceof Error, 'should be instanceof Error, got: ' + p(data)); + test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + p(data.message)); + test.equal(response.raw, 'Чебурашка', 'should be "Чебурашка", got: ' + p(response.raw)); + test.done(); + }).on('error', function(err) { + test.ok(err instanceof Error, 'should be instanceof Error, got: ' + p(err)); + }).on('success', function() { + test.ok(false, 'should not have got here'); + }).on('fail', function() { + test.ok(false, 'should not have got here'); + }); + }, + + 'Should correctly handle malformed XML': function(test) { + test.expect(4); + rest.get(host + '/mal-xml').on('complete', function(data, response) { + test.ok(data instanceof Error, 'should be instanceof Error, got: ' + p(data)); + test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + p(data.message)); + test.equal(response.raw, 'Чебурашка', 'should be "Чебурашка", got: ' + p(response.raw)); + test.done(); + }).on('error', function(err) { + test.ok(err instanceof Error, 'should be instanceof Error, got: ' + p(err)); + }).on('success', function() { + test.ok(false, 'should not have got here'); + }).on('fail', function() { + test.ok(false, 'should not have got here'); + }); + }, + + 'Should correctly handle malformed YAML': function(test) { + test.expect(4); + rest.get(host + '/mal-yaml').on('complete', function(data, response) { + test.ok(data instanceof Error, 'should be instanceof Error, got: ' + p(data)); + test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + p(data.message)); + test.equal(response.raw, '{Чебурашка', 'should be "{Чебурашка", got: ' + p(response.raw)); + test.done(); + }).on('error', function(err) { + test.ok(err instanceof Error, 'should be instanceof Error, got: ' + p(err)); + }).on('success', function() { + test.ok(false, 'should not have got here'); + }).on('fail', function() { + test.ok(false, 'should not have got here'); + }); + } + +}; + +if (Iconv) { + module.exports['Deserialization']['Should correctly convert charsets '] = function(test) { + rest.get(host + '/charset').on('complete', function(data) { + test.equal(data, 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'); + test.done(); + }); + }; +} + + +function redirectResponse(request, response) { + if (request.url == '/redirected') { + response.writeHead(200, { 'content-type': 'text/plain' }); + response.end('redirected'); + } else if (request.url == '/') { + response.writeHead(301, { + 'location': host + '/' + (request.headers['x-redirects'] ? '1' : 'redirected') + }); + response.end('redirect'); + } else { + var count = parseInt(request.url.substr(1)); + var max = parseInt(request.headers['x-redirects']); + response.writeHead(count < max ? 301 : 200, { + 'location': host + '/' + (count + 1) + }); + response.end(count.toString(10)); + } +} + +module.exports['Redirect'] = { + + setUp: setup(redirectResponse), + tearDown: teardown(), + + 'Should follow redirects': function(test) { + rest.get(host).on('complete', function(data) { + test.equal(data, 'redirected', 'returned: ' + p(data)); + test.done(); + }); + }, + + 'Should follow multiple redirects': function(test) { + rest.get(host, { + headers: { 'x-redirects': '5' } + }).on('complete', function(data) { + test.equal(data, '5', 'returned: ' + p(data)); + test.done(); + }); + } + +}; + + +function contentLengthResponse(request, response) { + response.writeHead(200, { 'content-type': 'text/plain' }); + if ('content-length' in request.headers) { + response.write(request.headers['content-length']); + } else { + response.write('content-length is not set'); + } + response.end(); +} + +module.exports['Content-Length'] = { + + setUp: setup(contentLengthResponse), + tearDown: teardown(), + + 'JSON content length': function(test) { + rest.post(host, { + data: JSON.stringify({ greeting: 'hello world' }) + }).on('complete', function(data) { + test.equal(26, data, 'should set content-length'); + test.done(); + }); + }, + + 'JSON multibyte content length': function (test) { + rest.post(host, { + data: JSON.stringify({ greeting: 'こんにちは世界' }) + }).on('complete', function(data) { + test.equal(36, data, 'should byte-size content-length'); + test.done(); + }); + } + +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1aa4fce --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "faceplate", + "version": "0.5.0", + "author": { + "name": "Heroku" + }, + "homepage": "https://github.com/heroku/faceplate", + "description": "Wrapper for Facebook authentication and API", + "repository": { + "type": "git", + "url": "https://github.com/heroku/faceplate" + }, + "dependencies": { + "b64url": "1.0.3", + "restler": "2.0.0" + }, + "readme": "# faceplate\n\nA Node.js wrapper for Facebook authentication and API\n\n## Usage\n\nUse as a connect middleware\n\n```javascript\n// create an express webserver\nvar app = require('express').createServer(\n express.bodyParser(),\n express.cookieParser(),\n require('faceplate').middleware({\n app_id: process.env.FACEBOOK_APP_ID,\n secret: process.env.FACEBOOK_SECRET,\n scope: 'user_likes,user_photos,user_photo_video_tags'\n })\n);\n\n// show friends\napp.get('/friends', function(req, res) {\n req.facebook.get('/me/friends', { limit: 4 }, function(friends) {\n res.send('friends: ' + require('util').inspect(friends));\n });\n});\n\n// use fql to show my friends using this app\napp.get('/friends_using_app', function(req, res) {\n req.facebook.fql('SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1', function(friends_using_app) {\n res.send('friends using app: ' + require('util').inspect(friends_using_app));\n });\n});\n\n// perform multiple fql queries at once\napp.get('/multiquery', function(req, res) {\n req.facebook.fql({\n likes: 'SELECT user_id, object_id, post_id FROM like WHERE user_id=me()',\n albums: 'SELECT object_id, cover_object_id, name FROM album WHERE owner=me()',\n },\n function(result) {\n var inspect = require('util').inspect;\n res.send('Yor likes: ' + inspect(result.likes) + ', your albums: ' + inspect(result.albums) );\n });\n});\n\n// See the full signed request details\napp.get('/signed_request', function(req, res) {\n res.send('Signed Request details: ' + require('util').inspect(req.facebook.signed_request));\n});\n\n```\n\n## License\n\nMIT\n", + "readmeFilename": "README.md", + "_id": "faceplate@0.5.0", + "dist": { + "shasum": "91c8fb217dd130bb88ac343a0aaeac27c720bb81" + }, + "_from": "faceplate@0.5.0", + "_resolved": "https://registry.npmjs.org/faceplate/-/faceplate-0.5.0.tgz" +} From a58e2c4c6f66f2823e064764f724439a40cfe942 Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Mon, 13 May 2013 14:18:52 -0400 Subject: [PATCH 09/16] something --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ba40526..ea10b21 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ var b64url = require('b64url'); var crypto = require('crypto'); var qs = require('querystring'); var restler = require('restler'); - + function safeParse(str){ // console.log('parsing str: ', str) From 5562feceab0f545b77c05318af6953b35be7fdee Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Mon, 13 May 2013 14:22:56 -0400 Subject: [PATCH 10/16] removed node modules --- README.md | 2 +- node_modules/b64url/lib/b64url.js | 26 - node_modules/b64url/package.json | 28 - node_modules/b64url/readme.md | 10 - node_modules/restler/.npmignore | 3 - node_modules/restler/MIT-LICENSE | 20 - node_modules/restler/README.md | 210 ------- node_modules/restler/bin/restler | 23 - node_modules/restler/index.js | 1 - node_modules/restler/lib/multipartform.js | 203 ------- node_modules/restler/lib/restler.js | 488 ---------------- node_modules/restler/package.json | 30 - node_modules/restler/test/all.js | 6 - node_modules/restler/test/restler.js | 672 ---------------------- package.json | 2 +- 15 files changed, 2 insertions(+), 1722 deletions(-) delete mode 100644 node_modules/b64url/lib/b64url.js delete mode 100644 node_modules/b64url/package.json delete mode 100644 node_modules/b64url/readme.md delete mode 100644 node_modules/restler/.npmignore delete mode 100644 node_modules/restler/MIT-LICENSE delete mode 100644 node_modules/restler/README.md delete mode 100755 node_modules/restler/bin/restler delete mode 100644 node_modules/restler/index.js delete mode 100644 node_modules/restler/lib/multipartform.js delete mode 100644 node_modules/restler/lib/restler.js delete mode 100644 node_modules/restler/package.json delete mode 100644 node_modules/restler/test/all.js delete mode 100644 node_modules/restler/test/restler.js diff --git a/README.md b/README.md index 01724dc..cfb84a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # faceplate - + A Node.js wrapper for Facebook authentication and API ## Usage diff --git a/node_modules/b64url/lib/b64url.js b/node_modules/b64url/lib/b64url.js deleted file mode 100644 index 6dbc81c..0000000 --- a/node_modules/b64url/lib/b64url.js +++ /dev/null @@ -1,26 +0,0 @@ -function rtrim(data, chr) { - var drop = 0 - , len = data.length - while (data.charAt(len - 1 - drop) === chr) drop++ - return data.substr(0, len - drop) -} - -module.exports.safe = function(b64data) { - return rtrim(b64data, '=').replace(/\+/g, '-').replace(/\//g, '_') -} - -module.exports.encode = function(data) { - var buf = data - if (!(data instanceof Buffer)) { - buf = new Buffer(Buffer.byteLength(data)) - buf.write(data) - } - return exports.safe(buf.toString('base64')) -} - -module.exports.decode = function(data, encoding) { - encoding = encoding === undefined ? 'utf8' : encoding - var buf = new Buffer(data.replace(/-/g, '+').replace(/_/g, '/'), 'base64') - if (!encoding) return buf - return buf.toString(encoding) -} diff --git a/node_modules/b64url/package.json b/node_modules/b64url/package.json deleted file mode 100644 index 1eabe1f..0000000 --- a/node_modules/b64url/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "b64url", - "description": "URL safe base64 encoding/decoding.", - "version": "1.0.3", - "homepage": "https://github.com/nshah/nodejs-b64url", - "author": { - "name": "Naitik Shah", - "email": "n@daaku.org" - }, - "main": "lib/b64url", - "repository": { - "type": "git", - "url": "https://github.com/nshah/nodejs-b64url" - }, - "scripts": { - "test": "./node_modules/.bin/expresso -c" - }, - "devDependencies": { - "expresso": ">= 0.8.1" - }, - "engines": { - "node": ">= 0.4.1" - }, - "readme": "b64url\n======\n\nURL safe base64 encoding/decoding as described\n[here](http://tools.ietf.org/html/rfc3548#section-4).\n\n```javascript\nvar encoded = b64url.encode(data)\nvar decoded = b64url.decode(encoded)\n```\n", - "readmeFilename": "readme.md", - "_id": "b64url@1.0.3", - "_from": "b64url@1.0.3" -} diff --git a/node_modules/b64url/readme.md b/node_modules/b64url/readme.md deleted file mode 100644 index 5c38a83..0000000 --- a/node_modules/b64url/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -b64url -====== - -URL safe base64 encoding/decoding as described -[here](http://tools.ietf.org/html/rfc3548#section-4). - -```javascript -var encoded = b64url.encode(data) -var decoded = b64url.decode(encoded) -``` diff --git a/node_modules/restler/.npmignore b/node_modules/restler/.npmignore deleted file mode 100644 index 32943a1..0000000 --- a/node_modules/restler/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -*.swp -.[Dd][Ss]_store -node_modules diff --git a/node_modules/restler/MIT-LICENSE b/node_modules/restler/MIT-LICENSE deleted file mode 100644 index cc25ab4..0000000 --- a/node_modules/restler/MIT-LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2009 Dan Webb - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/node_modules/restler/README.md b/node_modules/restler/README.md deleted file mode 100644 index 37ee3c6..0000000 --- a/node_modules/restler/README.md +++ /dev/null @@ -1,210 +0,0 @@ -Restler -======= - -(C) Dan Webb (dan@danwebb.net/@danwrong) 2011, Licensed under the MIT-LICENSE - -An HTTP client library for node.js (0.3 and up). Hides most of the complexity of creating and using http.Client. Very early days yet. - -**Release 2.x.x** will be dedicated to modifying how errors are handled and emitted. Currently errors are being fired as an on 'error' event but as [@ctavan](https://github.com/ctavan) pointed out on [issue #36](https://github.com/danwrong/restler/pull/36) a better approach (and more commonly in vogue now) would be to pass the error obj to the callback. - -Ths change will inevitably affect those using older < 0.2.x versions of restler. Those not ready to upgrade yet are encouraged to stay on the 0.2.x version. - -See [Version History](https://github.com/danwrong/restler/wiki/Version-History) for changes - - -Features --------- - -* Easy interface for common operations via http.request -* Automatic serialization of post data -* Automatic serialization of query string data -* Automatic deserialization of XML, JSON and YAML responses to JavaScript objects (if you have js-yaml and/or xml2js in the require path) -* Provide your own deserialization functions for other datatypes -* Automatic following of redirects -* Send files with multipart requests -* Transparently handle SSL (just specify https in the URL) -* Deals with basic auth for you, just provide username and password options -* Simple service wrapper that allows you to easily put together REST API libraries -* Transparently handle content-encoded responses (gzip, deflate) (requires node 0.6+) -* Transparently handle different content charsets via [iconv](https://github.com/bnoordhuis/node-iconv) (if available) - - -API ---- - -### request(url, options) - -Basic method to make a request of any type. The function returns a RestRequest object that emits events: - -#### events - -* `complete: function(result, response)` - emitted when the request has finished whether it was successful or not. Gets passed the response result and the response object as arguments. If some error has occurred, `result` is always instance of `Error`, otherwise it contains response data. -* `success: function(data, response)` - emitted when the request was successful. Gets passed the response data and the response object as arguments. -* `fail: function(data, response)` - emitted when the request was successful, but 4xx status code returned. Gets passed the response data and the response object as arguments. -* `error: function(err, response)` - emitted when some errors have occurred (eg. connection aborted, parse, encoding, decoding failed or some other unhandled errors). Gets passed the `Error` object and the response object (when available) as arguments. -* `abort: function()` - emitted when `request.abort()` is called. -* `2XX`, `3XX`, `4XX`, `5XX: function(data, response)` - emitted for all requests with response codes in the range (eg. `2XX` emitted for 200, 201, 203). -* actual response code: function(data, response) - emitted for every single response code (eg. 404, 201, etc). - -#### members - -* `abort([error])` Cancels request. `abort` event is emitted. `request.aborted` is set to `true`. If non-falsy `error` is passed, then `error` will be additionaly emitted (with `error` passed as a param and `error.type` is set to `"abort"`). Otherwise only `complete` event will raise. -* `retry([timeout])` Re-sends request after `timeout` ms. Pending request is aborted. -* `aborted` Determines if request was aborted. - - -### get(url, options) - -Create a GET request. - -### post(url, options) - -Create a POST request. - -### put(url, options) - -Create a PUT request. - -### del(url, options) - -Create a DELETE request. - -### head(url, options) - -Create a HEAD request. - -### json(url, data, options) - -Send json `data` via GET method. - -### postJson(url, data, options) - -Send json `data` via POST method. - - -### Parsers - -You can give any of these to the parsers option to specify how the response data is deserialized. -In case of malformed content, parsers emit `error` event. Original data returned by server is stored in `response.raw`. - -#### parsers.auto - -Checks the content-type and then uses parsers.xml, parsers.json or parsers.yaml. -If the content type isn't recognised it just returns the data untouched. - -#### parsers.json, parsers.xml, parsers.yaml - -All of these attempt to turn the response into a JavaScript object. In order to use the YAML and XML parsers you must have yaml and/or xml2js installed. - -### Options - -* `method` Request method, can be get, post, put, del. Defaults to `"get"`. -* `query` Query string variables as a javascript object, will override the querystring in the URL. Defaults to empty. -* `data` The data to be added to the body of the request. Can be a string or any object. -Note that if you want your request body to be JSON with the `Content-Type: application/json`, you need to -`JSON.stringify` your object first. Otherwise, it will be sent as `application/x-www-form-urlencoded` and encoded accordingly. -Also you can use `json()` and `postJson()` methods. -* `parser` A function that will be called on the returned data. Use any of predefined `restler.parsers`. See parsers section below. Defaults to `restler.parsers.auto`. -* `encoding` The encoding of the request body. Defaults to `"utf8"`. -* `decoding` The encoding of the response body. For a list of supported values see [Buffers](http://nodejs.org/docs/latest/api/buffers.html#buffers). Additionally accepts `"buffer"` - returns response as `Buffer`. Defaults to `"utf8"`. -* `headers` A hash of HTTP headers to be sent. Defaults to `{ 'Accept': '*/*', 'User-Agent': 'Restler for node.js' }`. -* `username` Basic auth username. Defaults to empty. -* `password` Basic auth password. Defaults to empty. -* `multipart` If set the data passed will be formated as `multipart/form-encoded`. See multipart example below. Defaults to `false`. -* `client` A http.Client instance if you want to reuse or implement some kind of connection pooling. Defaults to empty. -* `followRedirects` If set will recursively follow redirects. Defaults to `true`. - - -Example usage -------------- - -```javascript -var sys = require('util'), - rest = require('./restler'); - -rest.get('http://google.com').on('complete', function(result) { - if (result instanceof Error) { - sys.puts('Error: ' + result.message); - this.retry(5000); // try again after 5 sec - } else { - sys.puts(result); - } -}); - -rest.get('http://twaud.io/api/v1/users/danwrong.json').on('complete', function(data) { - sys.puts(data[0].message); // auto convert to object -}); - -rest.get('http://twaud.io/api/v1/users/danwrong.xml').on('complete', function(data) { - sys.puts(data[0].sounds[0].sound[0].message); // auto convert to object -}); - -rest.post('http://user:pass@service.com/action', { - data: { id: 334 }, -}).on('complete', function(data, response) { - if (response.statusCode == 201) { - // you can get at the raw response like this... - } -}); - -// multipart request sending a file and using https -rest.post('https://twaud.io/api/v1/upload.json', { - multipart: true, - username: 'danwrong', - password: 'wouldntyouliketoknow', - data: { - 'sound[message]': 'hello from restler!', - 'sound[file]': rest.file('doug-e-fresh_the-show.mp3', null, null, null, 'audio/mpeg') - } -}).on('complete', function(data) { - sys.puts(data.audio_url); -}); - -// create a service constructor for very easy API wrappers a la HTTParty... -Twitter = rest.service(function(u, p) { - this.defaults.username = u; - this.defaults.password = p; -}, { - baseURL: 'http://twitter.com' -}, { - update: function(message) { - return this.post('/statuses/update.json', { data: { status: message } }); - } -}); - -var client = new Twitter('danwrong', 'password'); -client.update('Tweeting using a Restler service thingy').on('complete', function(data) { - sys.p(data); -}); - -// post JSON -var jsonData = { id: 334 }; -rest.postJson('http://example.com/action', jsonData).on('complete', function(data, response) { - // handle response -}); - -``` - -Running the tests ------------------ -install **[nodeunit](https://github.com/caolan/nodeunit)** - -```bash -npm install nodeunit -``` - -then - -```bash -node test/all.js -``` - -or - -```bash -nodeunit test/restler.js -``` - -TODO ----- -* What do you need? Let me know or fork. diff --git a/node_modules/restler/bin/restler b/node_modules/restler/bin/restler deleted file mode 100755 index 8f112de..0000000 --- a/node_modules/restler/bin/restler +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env node - -require.paths.push(process.cwd()); - -var sys = require('util'), - rest = require('../lib/restler'), - repl = require('repl'); - -var replServer = repl.start(); - -var exportMethods = { - sys: sys, - rest: rest -} - -Object.keys(exportMethods).forEach(function(exportMethod) { - replServer.context[exportMethod] = exportMethods[exportMethod]; -}); - -rest.get('http://twaud.io/api/v1/7.json').on('complete', function(data, response) { - console.log(response.headers); - replServer.context.data = data; -}); \ No newline at end of file diff --git a/node_modules/restler/index.js b/node_modules/restler/index.js deleted file mode 100644 index 9b280e8..0000000 --- a/node_modules/restler/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./lib/restler'); \ No newline at end of file diff --git a/node_modules/restler/lib/multipartform.js b/node_modules/restler/lib/multipartform.js deleted file mode 100644 index 215d032..0000000 --- a/node_modules/restler/lib/multipartform.js +++ /dev/null @@ -1,203 +0,0 @@ -var fs = require('fs'); -var sys = require('util') -exports.defaultBoundary = '48940923NODERESLTER3890457293'; - - -// This little object allows us hijack the write method via duck-typing -// and write to strings or regular streams that support the write method. -function Stream(stream) { - //If the user pases a string for stream,we initalize one to write to - if (this._isString(stream)) { - this.string = ""; - } - this.stream = stream; - -} - -Stream.prototype = { - //write to an internal String or to the Stream - write: function(data) { - if (this.string != undefined) { - this.string += data; - } else { - this.stream.write(data, "binary"); - } - }, - - //stolen from underscore.js - _isString: function(obj) { - return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); - } -} - -function File(path, filename, fileSize, encoding, contentType) { - this.path = path; - this.filename = filename || this._basename(path); - this.fileSize = fileSize; - this.encoding = encoding || "binary"; - this.contentType = contentType || 'application/octet-stream'; -} - -File.prototype = { - _basename: function(path) { - var parts = path.split(/\/|\\/); - return parts[parts.length - 1]; - } -}; - -function Data(filename, contentType, data) { - this.filename = filename; - this.contentType = contentType || 'application/octet-stream'; - this.data = data; -} - -function Part(name, value, boundary) { - this.name = name; - this.value = value; - this.boundary = boundary; -} - - -Part.prototype = { - - //returns the Content-Disposition header - header: function() { - var header; - - if (this.value.data) { - header = "Content-Disposition: form-data; name=\"" + this.name + - "\"; filename=\"" + this.value.filename + "\"\r\n" + - "Content-Type: " + this.value.contentType; - } if (this.value instanceof File) { - header = "Content-Disposition: form-data; name=\"" + this.name + - "\"; filename=\"" + this.value.filename + "\"\r\n" + - "Content-Length: " + this.value.fileSize + "\r\n" + - "Content-Type: " + this.value.contentType; - } else { - header = "Content-Disposition: form-data; name=\"" + this.name + "\""; - } - - return "--" + this.boundary + "\r\n" + header + "\r\n\r\n"; - }, - - //calculates the size of the Part - sizeOf: function() { - var valueSize; - if (this.value instanceof File) { - valueSize = this.value.fileSize; - } else if (this.value.data) { - valueSize = this.value.data.length; - } else { - valueSize = this.value.length; - } - return valueSize + this.header().length + 2; - }, - - // Writes the Part out to a writable stream that supports the write(data) method - // You can also pass in a String and a String will be returned to the callback - // with the whole Part - // Calls the callback when complete - write: function(stream, callback) { - - var self = this; - - //first write the Content-Disposition - stream.write(this.header()); - - //Now write out the body of the Part - if (this.value instanceof File) { - fs.open(this.value.path, "r", 0666, function (err, fd) { - if (err) throw err; - - var position = 0; - - (function reader () { - fs.read(fd, 1024 * 4, position, "binary", function (er, chunk) { - if (er) callback(err); - stream.write(chunk); - position += 1024 * 4; - if (chunk) reader(); - else { - stream.write("\r\n") - callback(); - fs.close(fd); - } - }); - })(); // reader() - }); - } else { - stream.write(this.value + "\r\n"); - callback(); - } - } -} - -//Renamed to MultiPartRequest from Request -function MultiPartRequest(data, boundary) { - this.encoding = 'binary'; - this.boundary = boundary || exports.defaultBoundary; - this.data = data; - this.partNames = this._partNames(); -} - -MultiPartRequest.prototype = { - _partNames: function() { - var partNames = []; - for (var name in this.data) { - partNames.push(name) - } - return partNames; - }, - - write: function(stream, callback) { - var partCount = 0, self = this; - - // wrap the stream in our own Stream object - // See the Stream function above for the benefits of this - var stream = new Stream(stream); - - // Let each part write itself out to the stream - (function writePart() { - var partName = self.partNames[partCount]; - var part = new Part(partName, self.data[partName], self.boundary); - part.write(stream, function (err) { - if (err) { - callback(err); - return; - } - partCount += 1; - if (partCount < self.partNames.length) - writePart(); - else { - stream.write('--' + self.boundary + '--' + "\r\n"); - - if (callback) callback(stream.string || ""); - } - }); - })(); - } -} - -var exportMethods = { - file: function(path, filename, fileSize, encoding, contentType) { - return new File(path, filename, fileSize, encoding, contentType) - }, - data: function(filename, contentType, data) { - return new Data(filename, contentType, data); - }, - sizeOf: function(parts, boundary) { - var totalSize = 0; - boundary = boundary || exports.defaultBoundary; - for (var name in parts) totalSize += new Part(name, parts[name], boundary).sizeOf(); - return totalSize + boundary.length + 6; - }, - write: function(stream, data, callback, boundary) { - var r = new MultiPartRequest(data, boundary); - r.write(stream, callback); - return r; - } -} - -Object.keys(exportMethods).forEach(function(exportMethod) { - exports[exportMethod] = exportMethods[exportMethod] -}) diff --git a/node_modules/restler/lib/restler.js b/node_modules/restler/lib/restler.js deleted file mode 100644 index 7842164..0000000 --- a/node_modules/restler/lib/restler.js +++ /dev/null @@ -1,488 +0,0 @@ -var sys = require('util'); -var http = require('http'); -var https = require('https'); -var url = require('url'); -var qs = require('querystring'); -var multipart = require('./multipartform'); -var zlib = null; -var Iconv = null; - -try { - zlib = require('zlib'); -} catch (err) {} - -try { - Iconv = require('iconv').Iconv; -} catch (err) {} - -function mixin(target, source) { - source = source || {}; - Object.keys(source).forEach(function(key) { - target[key] = source[key]; - }); - - return target; -} - -function Request(uri, options) { - this.url = url.parse(uri); - this.options = options; - this.headers = { - 'Accept': '*/*', - 'User-Agent': 'Restler for node.js', - 'Host': this.url.host - }; - - if (zlib) { - this.headers['Accept-Encoding'] = 'gzip, deflate'; - } - - mixin(this.headers, options.headers || {}); - - // set port and method defaults - if (!this.url.port) this.url.port = (this.url.protocol == 'https:') ? '443' : '80'; - if (!this.options.method) this.options.method = (this.options.data) ? 'POST' : 'GET'; - if (typeof this.options.followRedirects == 'undefined') this.options.followRedirects = true; - - // stringify query given in options of not given in URL - if (this.options.query && !this.url.query) { - if (typeof this.options.query == 'object') - this.url.query = qs.stringify(this.options.query); - else this.url.query = this.options.query; - } - - this._applyBasicAuth(); - - if (this.options.multipart) { - this.headers['Content-Type'] = 'multipart/form-data; boundary=' + multipart.defaultBoundary; - } else { - if (typeof this.options.data == 'object') { - this.options.data = qs.stringify(this.options.data); - this.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - this.headers['Content-Length'] = this.options.data.length; - } - if(typeof this.options.data == 'string') { - var buffer = new Buffer(this.options.data, this.options.encoding || 'utf8'); - this.options.data = buffer; - this.headers['Content-Length'] = buffer.length; - } - } - - var proto = (this.url.protocol == 'https:') ? https : http; - - this.request = proto.request({ - host: this.url.hostname, - port: this.url.port, - path: this._fullPath(), - method: this.options.method, - headers: this.headers - }); - - this._makeRequest(); -} - -Request.prototype = new process.EventEmitter(); - -mixin(Request.prototype, { - _isRedirect: function(response) { - return ([301, 302, 303].indexOf(response.statusCode) >= 0); - }, - _fullPath: function() { - var path = this.url.pathname || '/'; - if (this.url.hash) path += this.url.hash; - if (this.url.query) path += '?' + this.url.query; - return path; - }, - _applyBasicAuth: function() { - var authParts; - - if (this.url.auth) { - authParts = this.url.auth.split(':'); - this.options.username = authParts[0]; - this.options.password = authParts[1]; - } - - if (this.options.username && this.options.password) { - var b = new Buffer([this.options.username, this.options.password].join(':')); - this.headers['Authorization'] = "Basic " + b.toString('base64'); - } - }, - _responseHandler: function(response) { - var self = this; - - if (self._isRedirect(response) && self.options.followRedirects) { - try { - self.url = url.parse(url.resolve(self.url.href, response.headers['location'])); - self._retry(); - // todo handle somehow infinite redirects - } catch(err) { - err.message = 'Failed to follow redirect: ' + err.message; - self._fireError(err, response); - } - } else { - var body = ''; - - response.setEncoding('binary'); - - response.on('data', function(chunk) { - body += chunk; - }); - - response.on('end', function() { - response.rawEncoded = body; - self._decode(new Buffer(body, 'binary'), response, function(err, body) { - if (err) { - self._fireError(err, response); - return; - } - response.raw = body; - body = self._iconv(body, response); - self._encode(body, response, function(err, body) { - if (err) { - self._fireError(err, response); - } else { - self._fireSuccess(body, response); - } - }); - }); - }); - } - }, - _decode: function(body, response, callback) { - var decoder = response.headers['content-encoding']; - if (decoder in decoders) { - decoders[decoder].call(response, body, callback); - } else { - callback(null, body); - } - }, - _iconv: function(body, response) { - if (Iconv) { - var charset = response.headers['content-type']; - if (charset) { - charset = /\bcharset=(.+)(?:;|$)/i.exec(charset); - if (charset) { - charset = charset[1].trim().toUpperCase(); - if (charset != 'UTF-8') { - try { - var iconv = new Iconv(charset, 'UTF-8//TRANSLIT//IGNORE'); - return iconv.convert(body); - } catch (err) {} - } - } - } - } - return body; - }, - _encode: function(body, response, callback) { - var self = this; - if (self.options.decoding == 'buffer') { - callback(null, body); - } else { - body = body.toString(self.options.decoding); - if (self.options.parser) { - self.options.parser.call(response, body, callback); - } else { - callback(null, body); - } - } - }, - _fireError: function(err, response) { - this.emit('error', err, response); - this.emit('complete', err, response); - }, - _fireSuccess: function(body, response) { - if (parseInt(response.statusCode) >= 400) { - this.emit('fail', body, response); - } else { - this.emit('success', body, response); - } - this.emit(response.statusCode.toString().replace(/\d{2}$/, 'XX'), body, response); - this.emit(response.statusCode.toString(), body, response); - this.emit('complete', body, response); - }, - _makeRequest: function() { - var self = this; - this.request.on('response', function(response) { - self._responseHandler(response); - }).on('error', function(err) { - if (!self.aborted) { - self._fireError(err, null); - } - }); - }, - _retry: function() { - this.request.removeAllListeners().on('error', function() {}); - if (this.request.finished) { - this.request.abort(); - } - Request.call(this, this.url.href, this.options); // reusing request object to handle recursive calls and remember listeners - this.run(); - }, - run: function() { - var self = this; - - if (this.options.multipart) { - multipart.write(this.request, this.options.data, function() { - self.request.end(); - }); - } else { - if (this.options.data) { - this.request.write(this.options.data.toString(), this.options.encoding || 'utf8'); - } - this.request.end(); - } - - return this; - }, - abort: function(err) { - var self = this; - - if (err) { - if (typeof err == 'string') { - err = new Error(err); - } else if (!(err instanceof Error)) { - err = new Error('AbortError'); - } - err.type = 'abort'; - } else { - err = null; - } - - self.request.on('close', function() { - if (err) { - self._fireError(err, null); - } else { - self.emit('complete', null, null); - } - }); - - self.aborted = true; - self.request.abort(); - self.emit('abort', err); - return this; - }, - retry: function(timeout) { - var self = this; - timeout = parseInt(timeout); - var fn = self._retry.bind(self); - if (!isFinite(timeout) || timeout <= 0) { - process.nextTick(fn, timeout); - } else { - setTimeout(fn, timeout); - } - return this; - } -}); - -function shortcutOptions(options, method) { - options = options || {}; - options.method = method; - options.parser = (typeof options.parser !== "undefined") ? options.parser : parsers.auto; - return options; -} - -function request(url, options) { - var request = new Request(url, options); - request.on('error', function() {}); - process.nextTick(request.run.bind(request)); - return request; -} - -function get(url, options) { - return request(url, shortcutOptions(options, 'GET')); -} - -function patch(url, options) { - return request(url, shortcutOptions(options, 'PATCH')); -} - -function post(url, options) { - return request(url, shortcutOptions(options, 'POST')); -} - -function put(url, options) { - return request(url, shortcutOptions(options, 'PUT')); -} - -function del(url, options) { - return request(url, shortcutOptions(options, 'DELETE')); -} - -function head(url, options) { - return request(url, shortcutOptions(options, 'HEAD')); -} - -function json(url, data, options, method) { - options = options || {}; - options.parser = (typeof options.parser !== "undefined") ? options.parser : parsers.auto; - options.headers = options.headers || {}; - options.headers['content-type'] = 'application/json'; - options.data = JSON.stringify(data); - options.method = method || 'GET'; - return request(url, options); -} - -function postJson(url, data, options) { - return json(url, data, options, 'POST'); -} - -var parsers = { - auto: function(data, callback) { - var contentType = this.headers['content-type']; - var contentParser; - if (contentType) { - contentType = contentType.replace(/;.+/, ''); // remove all except mime type (eg. text/html; charset=UTF-8) - if (contentType in parsers.auto.matchers) { - contentParser = parsers.auto.matchers[contentType]; - } else { - // custom (vendor) mime types - var parts = contentType.match(/^([\w-]+)\/vnd((?:\.(?:[\w-]+))+)\+([\w-]+)$/i); - if (parts) { - var type = parts[1]; - var vendors = parts[2].substr(1).split('.'); - var subtype = parts[3]; - var vendorType; - while (vendors.pop() && !(vendorType in parsers.auto.matchers)) { - vendorType = vendors.length - ? type + '/vnd.' + vendors.join('.') + '+' + subtype - : vendorType = type + '/' + subtype; - } - contentParser = parsers.auto.matchers[vendorType]; - } - } - } - if (typeof contentParser == 'function') { - contentParser.call(this, data, callback); - } else { - callback(null, data); - } - }, - json: function(data, callback) { - if (data && data.length) { - try { - callback(null, JSON.parse(data)); - } catch (err) { - err.message = 'Failed to parse JSON body: ' + err.message; - callback(err, null); - } - } else { - callback(null, null); - } - } -}; - -parsers.auto.matchers = { - 'application/json': parsers.json -}; - -try { - var yaml = require('yaml'); - - parsers.yaml = function(data, callback) { - if (data) { - try { - callback(null, yaml.eval(data)); - } catch (err) { - err.message = 'Failed to parse YAML body: ' + err.message; - callback(err, null); - } - } else { - callback(null, null); - } - }; - - parsers.auto.matchers['application/yaml'] = parsers.yaml; -} catch(e) {} - -try { - var xml2js = require('xml2js'); - - parsers.xml = function(data, callback) { - if (data) { - var parser = new xml2js.Parser(); - parser.parseString(data, function(err, data) { - if (err) { - err.message = 'Failed to parse XML body: ' + err.message; - } - callback(err, data); - }); - } else { - callback(null, null); - } - }; - - parsers.auto.matchers['application/xml'] = parsers.xml; -} catch(e) { } - -var decoders = { - gzip: function(buf, callback) { - zlib.gunzip(buf, callback); - }, - deflate: function(buf, callback) { - zlib.inflate(buf, callback); - } -}; - - -function Service(defaults) { - if (defaults.baseURL) { - this.baseURL = defaults.baseURL; - delete defaults.baseURL; - } - - this.defaults = defaults; -} - -mixin(Service.prototype, { - request: function(path, options) { - return request(this._url(path), this._withDefaults(options)); - }, - get: function(path, options) { - return get(this._url(path), this._withDefaults(options)); - }, - patch: function(path, options) { - return patch(this._url(path), this._withDefaults(options)); - }, - put: function(path, options) { - return put(this._url(path), this._withDefaults(options)); - }, - post: function(path, options) { - return post(this._url(path), this._withDefaults(options)); - }, - del: function(path, options) { - return del(this._url(path), this._withDefaults(options)); - }, - _url: function(path) { - if (this.baseURL) return url.resolve(this.baseURL, path); - else return path; - }, - _withDefaults: function(options) { - var o = mixin({}, this.defaults); - return mixin(o, options); - } -}); - -function service(constructor, defaults, methods) { - constructor.prototype = new Service(defaults || {}); - mixin(constructor.prototype, methods); - return constructor; -} - -mixin(exports, { - Request: Request, - Service: Service, - request: request, - service: service, - get: get, - patch: patch, - post: post, - put: put, - del: del, - head: head, - json: json, - postJson: postJson, - parsers: parsers, - file: multipart.file, - data: multipart.data -}); diff --git a/node_modules/restler/package.json b/node_modules/restler/package.json deleted file mode 100644 index 1dfde02..0000000 --- a/node_modules/restler/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "restler", - "version": "2.0.0", - "description": "An HTTP client library for node.js", - "contributors": [ - { - "name": "Dan Webb", - "email": "dan@danwebb.net" - } - ], - "homepage": "https://github.com/danwrong/restler", - "directories": { - "lib": "./lib" - }, - "main": "./lib/restler", - "engines": { - "node": ">= 0.6.x" - }, - "dependencies": {}, - "devDependencies": { - "nodeunit": ">=0.5.0", - "xml2js": ">=0.1.0", - "yaml": ">=0.2.0", - "iconv": ">=1.0.0" - }, - "readme": "Restler\n=======\n\n(C) Dan Webb (dan@danwebb.net/@danwrong) 2011, Licensed under the MIT-LICENSE\n\nAn HTTP client library for node.js (0.3 and up). Hides most of the complexity of creating and using http.Client. Very early days yet.\n\n**Release 2.x.x** will be dedicated to modifying how errors are handled and emitted. Currently errors are being fired as an on 'error' event but as [@ctavan](https://github.com/ctavan) pointed out on [issue #36](https://github.com/danwrong/restler/pull/36) a better approach (and more commonly in vogue now) would be to pass the error obj to the callback.\n\nThs change will inevitably affect those using older < 0.2.x versions of restler. Those not ready to upgrade yet are encouraged to stay on the 0.2.x version.\n\nSee [Version History](https://github.com/danwrong/restler/wiki/Version-History) for changes\n\n\nFeatures\n--------\n\n* Easy interface for common operations via http.request\n* Automatic serialization of post data\n* Automatic serialization of query string data\n* Automatic deserialization of XML, JSON and YAML responses to JavaScript objects (if you have js-yaml and/or xml2js in the require path)\n* Provide your own deserialization functions for other datatypes\n* Automatic following of redirects\n* Send files with multipart requests\n* Transparently handle SSL (just specify https in the URL)\n* Deals with basic auth for you, just provide username and password options\n* Simple service wrapper that allows you to easily put together REST API libraries\n* Transparently handle content-encoded responses (gzip, deflate) (requires node 0.6+)\n* Transparently handle different content charsets via [iconv](https://github.com/bnoordhuis/node-iconv) (if available)\n\n\nAPI\n---\n\n### request(url, options)\n\nBasic method to make a request of any type. The function returns a RestRequest object that emits events:\n\n#### events\n\n* `complete: function(result, response)` - emitted when the request has finished whether it was successful or not. Gets passed the response result and the response object as arguments. If some error has occurred, `result` is always instance of `Error`, otherwise it contains response data.\n* `success: function(data, response)` - emitted when the request was successful. Gets passed the response data and the response object as arguments.\n* `fail: function(data, response)` - emitted when the request was successful, but 4xx status code returned. Gets passed the response data and the response object as arguments.\n* `error: function(err, response)` - emitted when some errors have occurred (eg. connection aborted, parse, encoding, decoding failed or some other unhandled errors). Gets passed the `Error` object and the response object (when available) as arguments.\n* `abort: function()` - emitted when `request.abort()` is called.\n* `2XX`, `3XX`, `4XX`, `5XX: function(data, response)` - emitted for all requests with response codes in the range (eg. `2XX` emitted for 200, 201, 203).\n* actual response code: function(data, response) - emitted for every single response code (eg. 404, 201, etc).\n\n#### members\n\n* `abort([error])` Cancels request. `abort` event is emitted. `request.aborted` is set to `true`. If non-falsy `error` is passed, then `error` will be additionaly emitted (with `error` passed as a param and `error.type` is set to `\"abort\"`). Otherwise only `complete` event will raise.\n* `retry([timeout])` Re-sends request after `timeout` ms. Pending request is aborted.\n* `aborted` Determines if request was aborted.\n\n\n### get(url, options)\n\nCreate a GET request.\n\n### post(url, options)\n\nCreate a POST request.\n\n### put(url, options)\n\nCreate a PUT request.\n\n### del(url, options)\n\nCreate a DELETE request.\n\n### head(url, options)\n\nCreate a HEAD request.\n\n### json(url, data, options)\n\nSend json `data` via GET method.\n\n### postJson(url, data, options)\n\nSend json `data` via POST method.\n\n\n### Parsers\n\nYou can give any of these to the parsers option to specify how the response data is deserialized.\nIn case of malformed content, parsers emit `error` event. Original data returned by server is stored in `response.raw`.\n\n#### parsers.auto\n\nChecks the content-type and then uses parsers.xml, parsers.json or parsers.yaml.\nIf the content type isn't recognised it just returns the data untouched.\n\n#### parsers.json, parsers.xml, parsers.yaml\n\nAll of these attempt to turn the response into a JavaScript object. In order to use the YAML and XML parsers you must have yaml and/or xml2js installed.\n\n### Options\n\n* `method` Request method, can be get, post, put, del. Defaults to `\"get\"`.\n* `query` Query string variables as a javascript object, will override the querystring in the URL. Defaults to empty.\n* `data` The data to be added to the body of the request. Can be a string or any object.\nNote that if you want your request body to be JSON with the `Content-Type: application/json`, you need to\n`JSON.stringify` your object first. Otherwise, it will be sent as `application/x-www-form-urlencoded` and encoded accordingly.\nAlso you can use `json()` and `postJson()` methods.\n* `parser` A function that will be called on the returned data. Use any of predefined `restler.parsers`. See parsers section below. Defaults to `restler.parsers.auto`.\n* `encoding` The encoding of the request body. Defaults to `\"utf8\"`.\n* `decoding` The encoding of the response body. For a list of supported values see [Buffers](http://nodejs.org/docs/latest/api/buffers.html#buffers). Additionally accepts `\"buffer\"` - returns response as `Buffer`. Defaults to `\"utf8\"`.\n* `headers` A hash of HTTP headers to be sent. Defaults to `{ 'Accept': '*/*', 'User-Agent': 'Restler for node.js' }`.\n* `username` Basic auth username. Defaults to empty.\n* `password` Basic auth password. Defaults to empty.\n* `multipart` If set the data passed will be formated as `multipart/form-encoded`. See multipart example below. Defaults to `false`.\n* `client` A http.Client instance if you want to reuse or implement some kind of connection pooling. Defaults to empty.\n* `followRedirects` If set will recursively follow redirects. Defaults to `true`.\n\n\nExample usage\n-------------\n\n```javascript\nvar sys = require('util'),\n rest = require('./restler');\n\nrest.get('http://google.com').on('complete', function(result) {\n if (result instanceof Error) {\n sys.puts('Error: ' + result.message);\n this.retry(5000); // try again after 5 sec\n } else {\n sys.puts(result);\n }\n});\n\nrest.get('http://twaud.io/api/v1/users/danwrong.json').on('complete', function(data) {\n sys.puts(data[0].message); // auto convert to object\n});\n\nrest.get('http://twaud.io/api/v1/users/danwrong.xml').on('complete', function(data) {\n sys.puts(data[0].sounds[0].sound[0].message); // auto convert to object\n});\n\nrest.post('http://user:pass@service.com/action', {\n data: { id: 334 },\n}).on('complete', function(data, response) {\n if (response.statusCode == 201) {\n // you can get at the raw response like this...\n }\n});\n\n// multipart request sending a file and using https\nrest.post('https://twaud.io/api/v1/upload.json', {\n multipart: true,\n username: 'danwrong',\n password: 'wouldntyouliketoknow',\n data: {\n 'sound[message]': 'hello from restler!',\n 'sound[file]': rest.file('doug-e-fresh_the-show.mp3', null, null, null, 'audio/mpeg')\n }\n}).on('complete', function(data) {\n sys.puts(data.audio_url);\n});\n\n// create a service constructor for very easy API wrappers a la HTTParty...\nTwitter = rest.service(function(u, p) {\n this.defaults.username = u;\n this.defaults.password = p;\n}, {\n baseURL: 'http://twitter.com'\n}, {\n update: function(message) {\n return this.post('/statuses/update.json', { data: { status: message } });\n }\n});\n\nvar client = new Twitter('danwrong', 'password');\nclient.update('Tweeting using a Restler service thingy').on('complete', function(data) {\n sys.p(data);\n});\n\n// post JSON\nvar jsonData = { id: 334 };\nrest.postJson('http://example.com/action', jsonData).on('complete', function(data, response) {\n // handle response\n});\n\n```\n\nRunning the tests\n-----------------\ninstall **[nodeunit](https://github.com/caolan/nodeunit)**\n\n```bash\nnpm install nodeunit\n```\n\nthen\n\n```bash\nnode test/all.js\n```\n\nor\n\n```bash\nnodeunit test/restler.js\n```\n\nTODO\n----\n* What do you need? Let me know or fork.\n", - "readmeFilename": "README.md", - "_id": "restler@2.0.0", - "_from": "restler@2.0.0" -} diff --git a/node_modules/restler/test/all.js b/node_modules/restler/test/all.js deleted file mode 100644 index 12f3044..0000000 --- a/node_modules/restler/test/all.js +++ /dev/null @@ -1,6 +0,0 @@ - -require('./restler'); // debug -var nodeunit = require('nodeunit'); -var reporter = nodeunit.reporters['default']; -process.chdir(__dirname); -reporter.run(['restler.js']); diff --git a/node_modules/restler/test/restler.js b/node_modules/restler/test/restler.js deleted file mode 100644 index 1165f9b..0000000 --- a/node_modules/restler/test/restler.js +++ /dev/null @@ -1,672 +0,0 @@ - -var rest = require('../lib/restler'); -var http = require('http'); -var sys = require('util'); -var path = require('path'); -var fs = require('fs'); -var crypto = require('crypto'); -var zlib = null; -var Iconv = null; - -try { - zlib = require('zlib'); -} catch (err) {} - -try { - Iconv = require('iconv').Iconv; -} catch (err) {} - -var p = sys.inspect; - -var port = 9000; -var hostname = 'localhost'; -var host = 'http://' + hostname + ':' + port; - -var nodeunit = require('nodeunit'); -nodeunit.assert.re = function(actual, expected, message) { - new RegExp(expected).test(actual) || nodeunit.assert.fail(actual, expected, message, '~=', nodeunit.assert.re); -}; - - -function setup(response) { - return function(next) { - this.server = http.createServer(response); - this.server.listen(port, hostname, next); - }; -} - -function teardown() { - return function (next) { - this.server._handle && this.server.close(); - process.nextTick(next); - }; -} - - -function echoResponse(request, response) { - if (request.headers['x-connection-abort'] == 'true') { - request.connection.destroy(); - return; - } - var echo = []; - echo.push(request.method + ' ' + request.url + ' HTTP/' + request.httpVersion); - for (var header in request.headers) { - echo.push(header + ': ' + request.headers[header]); - } - echo.push('', ''); - echo = echo.join('\r\n'); - - request.addListener('data', function(chunk) { - echo += chunk.toString('binary'); - }); - - request.addListener('end', function() { - response.writeHead(request.headers['x-status-code'] || 200, { - 'content-type': 'text/plain', - 'content-length': echo.length, - 'request-method': request.method.toLowerCase() - }); - setTimeout(function() { - response.end(request.method == 'HEAD' ? undefined : echo); - }, request.headers['x-delay'] | 0); - }); -} - -module.exports['Basic'] = { - - setUp: setup(echoResponse), - tearDown: teardown(), - - 'Should GET': function (test) { - rest.get(host).on('complete', function(data) { - test.re(data, /^GET/, 'should be GET'); - test.done(); - }); - }, - - 'Should PATCH': function(test) { - rest.patch(host).on('complete', function(data) { - test.re(data, /^PATCH/, 'should be PATCH'); - test.done(); - }); - }, - - 'Should PUT': function(test) { - rest.put(host).on('complete', function(data) { - test.re(data, /^PUT/, 'should be PUT'); - test.done(); - }); - }, - - 'Should POST': function(test) { - rest.post(host).on('complete', function(data) { - test.re(data, /^POST/, 'should be POST'); - test.done(); - }); - }, - - 'Should DELETE': function(test) { - rest.del(host).on('complete', function(data) { - test.re(data, /^DELETE/, 'should be DELETE'); - test.done(); - }); - }, - - 'Should HEAD': function(test) { - rest.head(host).on('complete', function(data, response) { - test.equal(response.headers['request-method'], 'head', 'should be HEAD'); - test.done(); - }); - }, - - 'Should GET withouth path': function(test) { - rest.get(host).on('complete', function(data) { - test.re(data, /^GET \//, 'should hit /'); - test.done(); - }); - }, - - 'Should GET path': function(test) { - rest.get(host + '/thing').on('complete', function(data) { - test.re(data, /^GET \/thing/, 'should hit /thing'); - test.done(); - }); - }, - - 'Should preserve query string in url': function(test) { - rest.get(host + '/thing?boo=yah').on('complete', function(data) { - test.re(data, /^GET \/thing\?boo\=yah/, 'should hit /thing?boo=yah'); - test.done(); - }); - }, - - 'Should serialize query': function(test) { - rest.get(host, { query: { q: 'balls' } }).on('complete', function(data) { - test.re(data, /^GET \/\?q\=balls/, 'should hit /?q=balls'); - test.done(); - }); - }, - - 'Should POST body': function(test) { - rest.post(host, { data: 'balls' }).on('complete', function(data) { - test.re(data, /\r\n\r\nballs/, 'should have balls in the body'); - test.done(); - }); - }, - - 'Should serialize POST body': function(test) { - rest.post(host, { data: { q: 'balls' } }).on('complete', function(data) { - test.re(data, /content-type\: application\/x-www-form-urlencoded/, 'should set content-type'); - test.re(data, /content-length\: 7/, 'should set content-length'); - test.re(data, /\r\n\r\nq=balls/, 'should have balls in the body'); - test.done(); - }); - }, - - 'Should send headers': function(test) { - rest.get(host, { - headers: { 'Content-Type': 'application/json' } - }).on('complete', function(data) { - test.re(data, /content\-type\: application\/json/, 'should have "content-type" header'); - test.done(); - }); - }, - - 'Should send basic auth': function(test) { - rest.post(host, { username: 'danwrong', password: 'flange' }).on('complete', function(data) { - test.re(data, /authorization\: Basic ZGFud3Jvbmc6Zmxhbmdl/, 'should have "authorization "header'); - test.done(); - }); - }, - - 'Should send basic auth if in url': function(test) { - rest.post('http://danwrong:flange@' + hostname + ':' + port).on('complete', function(data) { - test.re(data, /authorization\: Basic ZGFud3Jvbmc6Zmxhbmdl/, 'should have "authorization" header'); - test.done(); - }); - }, - - 'Should fire 2XX and 200 events': function(test) { - test.expect(3); - rest.get(host).on('2XX', function() { - test.ok(true); - }).on('200', function() { - test.ok(true); - }).on('complete', function() { - test.ok(true); - test.done(); - }); - }, - - 'Should fire fail, 4XX and 404 events for 404': function(test) { - test.expect(4); - rest.get(host, { headers: { 'x-status-code': 404 }}).on('fail', function() { - test.ok(true); - }).on('4XX', function() { - test.ok(true); - }).on('404', function() { - test.ok(true); - }).on('complete', function() { - test.ok(true); - test.done(); - }); - }, - - 'Should fire error and complete events on connection abort': function(test) { - test.expect(2); - rest.get(host, { headers: { 'x-connection-abort': 'true' }}).on('error', function() { - test.ok(true); - }).on('complete', function() { - test.ok(true); - test.done(); - }); - }, - - 'Should correctly retry': function(test) { - var counter = 0; - rest.get(host, { headers: { 'x-connection-abort': 'true' }}).on('complete', function() { - if (++counter < 3) { - this.retry(10); - } else { - test.ok(true); - test.done(); - } - }); - }, - - 'Should correctly retry after abort': function(test) { - var counter = 0; - rest.get(host).on('complete', function() { - if (++counter < 3) { - this.retry().abort(); - } else { - test.ok(true); - test.done(); - } - }).abort(); - }, - - 'Should correctly retry while pending': function(test) { - var counter = 0, request; - function command() { - var args = [].slice.call(arguments); - var method = args.shift(); - method && setTimeout(function() { - request[method](); - command.apply(null, args); - }, 50); - } - - request = rest.get(host, { headers: { 'x-delay': '1000' } }).on('complete', function() { - if (++counter < 3) { - command('retry', 'abort'); - } else { - test.ok(true); - test.done(); - } - }); - command('abort'); - } - -}; - -module.exports['Multipart'] = { - - setUp: setup(echoResponse), - tearDown: teardown(), - - 'Test multipart request with simple vars': function(test) { - rest.post(host, { - data: { a: 1, b: 'thing' }, - multipart: true - }).on('complete', function(data) { - test.re(data, /content-type\: multipart\/form-data/, 'should set "content-type" header') - test.re(data, /name="a"(\s)+1/, 'should send a=1'); - test.re(data, /name="b"(\s)+thing/, 'should send b=thing'); - test.done(); - }); - } - -}; - - -function dataResponse(request, response) { - switch (request.url) { - case '/json': - response.writeHead(200, { 'content-type': 'application/json' }); - response.end('{ "ok": true }'); - break; - case '/xml': - response.writeHead(200, { 'content-type': 'application/xml' }); - response.end('true'); - break; - case '/yaml': - response.writeHead(200, { 'content-type': 'application/yaml' }); - response.end('ok: true'); - break; - case '/gzip': - response.writeHead(200, { 'content-encoding': 'gzip' }); - response.end(Buffer('H4sIAAAAAAAAA0vOzy0oSi0uTk1RSEksSUweHFwADdgOgJYAAAA=', 'base64')); - break; - case '/deflate': - response.writeHead(200, { 'content-encoding': 'deflate' }); - response.end(Buffer('eJxLzs8tKEotLk5NUUhJLElMHhxcAI9GO1c=', 'base64')); - break; - case '/truth': - response.writeHead(200, { - 'content-encoding': 'deflate', - 'content-type': 'application/json' - }); - response.end(Buffer('eJw1i0sKgDAQQ++S9Sj+cDFXEReCoy2UCv0oIt7dEZEsEvKSC4eZErjoCTaCr5uQjICHilQjYfLxkAD+g/IN3BCcXXT3GSF7u0uI2vjs3HubwW1ZdwRRcCZj/QpOIcv9ACXbJLo=', 'base64')); - break; - case '/binary': - response.writeHead(200); - response.end(Buffer([9, 30, 64, 135, 200])); - break; - case '/push-json': - var echo = ''; - request.addListener('data', function(chunk) { - echo += chunk.toString('binary'); - }); - request.addListener('end', function() { - response.writeHead(200, { - 'content-type': 'application/json' - }); - response.end(JSON.stringify(JSON.parse(echo))); - }); - break; - case '/custom-mime': - response.writeHead(200, { - 'content-type': 'application/vnd.github.beta.raw+json; charset=UTF-8' - }); - response.end(JSON.stringify([6,6,6])); - break; - case '/mal-json': - response.writeHead(200, { 'content-type': 'application/json' }); - response.end('Чебурашка'); - break; - case '/mal-xml': - response.writeHead(200, { 'content-type': 'application/xml' }); - response.end('Чебурашка'); - break; - case '/mal-yaml': - response.writeHead(200, { 'content-type': 'application/yaml' }); - response.end('{Чебурашка'); - break; - case '/abort': - setTimeout(function() { - response.writeHead(200); - response.end('not aborted'); - }, 100); - break; - case '/charset': - response.writeHead(200, { - 'content-type': 'text/plain; charset=windows-1251' - }); - response.end(Buffer('e0e1e2e3e4e5b8e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', 'hex')); - break; - default: - response.writeHead(404); - response.end(); - } -} - -module.exports['Deserialization'] = { - - setUp: setup(dataResponse), - tearDown: teardown(), - - 'Should parse JSON': function(test) { - rest.get(host + '/json').on('complete', function(data) { - test.equal(data.ok, true, 'returned: ' + p(data)); - test.done(); - }); - }, - - 'Should parse XML': function(test) { - rest.get(host + '/xml').on('complete', function(data, response) { - test.equal(data.ok, 'true', 'returned: ' + response.raw + ' || ' + p(data)); - test.done(); - }); - }, - - 'Should parse YAML': function(test) { - rest.get(host + '/yaml').on('complete', function(data) { - test.equal(data.ok, true, 'returned: ' + p(data)); - test.done(); - }); - }, - - 'Should gunzip': function(test) { - if (zlib) { - rest.get(host + '/gzip').on('complete', function(data) { - test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data)); - test.done(); - }); - } else { - test.done(); - } - }, - - 'Should inflate': function(test) { - if (zlib) { - rest.get(host + '/deflate').on('complete', function(data) { - test.re(data, /^(compressed data){10}$/, 'returned: ' + p(data)); - test.done(); - }) - } else { - test.done(); - } - }, - - 'Should decode and parse': function(test) { - if (zlib) { - rest.get(host + '/truth').on('complete', function(data) { - try { - with (data) { - var result = what + (is + the + answer + to + life + the + universe + and + everything).length; - } - test.equal(result, 42, 'returned: ' + p(data)); - } catch (err) { - test.ok(false, 'returned: ' + p(data)); - } - test.done(); - }) - } else { - test.done(); - } - }, - - 'Should decode as buffer': function(test) { - rest.get(host + '/binary', { decoding: 'buffer' }).on('complete', function(data) { - test.ok(data instanceof Buffer, 'should be buffer'); - test.equal(data.toString('base64'), 'CR5Ah8g=', 'returned: ' + p(data)); - test.done(); - }) - }, - - 'Should decode as binary': function(test) { - rest.get(host + '/binary', { decoding: 'binary' }).on('complete', function(data) { - test.ok(typeof data == 'string', 'should be string: ' + p(data)); - test.equal(data, '\t\u001e@‡È', 'returned: ' + p(data)); - test.done(); - }) - }, - - 'Should decode as base64': function(test) { - rest.get(host + '/binary', { decoding: 'base64' }).on('complete', function(data) { - test.ok(typeof data == 'string', 'should be string: ' + p(data)); - test.equal(data, 'CR5Ah8g=', 'returned: ' + p(data)); - test.done(); - }) - }, - - 'Should post and parse JSON': function(test) { - var obj = { secret : 'very secret string' }; - rest.post(host + '/push-json', { - headers: { - 'content-type': 'application/json' - }, - data: JSON.stringify(obj) - }).on('complete', function(data) { - test.equal(obj.secret, data.secret, 'returned: ' + p(data)); - test.done(); - }) - }, - - 'Should post and parse JSON via shortcut method': function(test) { - var obj = { secret : 'very secret string' }; - rest.postJson(host + '/push-json', obj).on('complete', function(data) { - test.equal(obj.secret, data.secret, 'returned: ' + p(data)); - test.done(); - }); - }, - - 'Should understand custom mime-type': function(test) { - rest.parsers.auto.matchers['application/vnd.github+json'] = function(data, callback) { - rest.parsers.json.call(this, data, function(err, data) { - err || (data.__parsedBy__ = 'github'); - callback(err, data); - }); - }; - rest.get(host + '/custom-mime').on('complete', function(data) { - test.expect(3); - test.ok(Array.isArray(data), 'should be array, returned: ' + p(data)); - test.equal(data.join(''), '666', 'should be [6,6,6], returned: ' + p(data)); - test.equal(data.__parsedBy__, 'github', 'should use vendor-specific parser, returned: ' + p(data.__parsedBy__)); - test.done(); - }); - }, - - 'Should correctly soft-abort request': function(test) { - test.expect(4); - rest.get(host + '/abort').on('complete', function(data) { - test.equal(data, null, 'data should be null'); - test.equal(this.aborted, true, 'should be aborted'); - test.done(); - }).on('error', function(err) { - test.ok(false, 'should not emit error event'); - }).on('abort', function(err) { - test.equal(err, null, 'err should be null'); - test.equal(this.aborted, true, 'should be aborted'); - }).on('success', function() { - test.ok(false, 'should not emit success event'); - }).on('fail', function() { - test.ok(false, 'should not emit fail event'); - }).abort(); - }, - - 'Should correctly hard-abort request': function(test) { - test.expect(4); - rest.get(host + '/abort').on('complete', function(data) { - test.ok(data instanceof Error, 'should be error, got: ' + p(data)); - test.equal(this.aborted, true, 'should be aborted'); - test.done(); - }).on('error', function(err) { - test.ok(err instanceof Error, 'should be error, got: ' + p(err)); - }).on('abort', function(err) { - test.equal(this.aborted, true, 'should be aborted'); - }).on('success', function() { - test.ok(false, 'should not emit success event'); - }).on('fail', function() { - test.ok(false, 'should not emit fail event'); - }).abort(true); - }, - - 'Should correctly handle malformed JSON': function(test) { - test.expect(4); - rest.get(host + '/mal-json').on('complete', function(data, response) { - test.ok(data instanceof Error, 'should be instanceof Error, got: ' + p(data)); - test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + p(data.message)); - test.equal(response.raw, 'Чебурашка', 'should be "Чебурашка", got: ' + p(response.raw)); - test.done(); - }).on('error', function(err) { - test.ok(err instanceof Error, 'should be instanceof Error, got: ' + p(err)); - }).on('success', function() { - test.ok(false, 'should not have got here'); - }).on('fail', function() { - test.ok(false, 'should not have got here'); - }); - }, - - 'Should correctly handle malformed XML': function(test) { - test.expect(4); - rest.get(host + '/mal-xml').on('complete', function(data, response) { - test.ok(data instanceof Error, 'should be instanceof Error, got: ' + p(data)); - test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + p(data.message)); - test.equal(response.raw, 'Чебурашка', 'should be "Чебурашка", got: ' + p(response.raw)); - test.done(); - }).on('error', function(err) { - test.ok(err instanceof Error, 'should be instanceof Error, got: ' + p(err)); - }).on('success', function() { - test.ok(false, 'should not have got here'); - }).on('fail', function() { - test.ok(false, 'should not have got here'); - }); - }, - - 'Should correctly handle malformed YAML': function(test) { - test.expect(4); - rest.get(host + '/mal-yaml').on('complete', function(data, response) { - test.ok(data instanceof Error, 'should be instanceof Error, got: ' + p(data)); - test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + p(data.message)); - test.equal(response.raw, '{Чебурашка', 'should be "{Чебурашка", got: ' + p(response.raw)); - test.done(); - }).on('error', function(err) { - test.ok(err instanceof Error, 'should be instanceof Error, got: ' + p(err)); - }).on('success', function() { - test.ok(false, 'should not have got here'); - }).on('fail', function() { - test.ok(false, 'should not have got here'); - }); - } - -}; - -if (Iconv) { - module.exports['Deserialization']['Should correctly convert charsets '] = function(test) { - rest.get(host + '/charset').on('complete', function(data) { - test.equal(data, 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'); - test.done(); - }); - }; -} - - -function redirectResponse(request, response) { - if (request.url == '/redirected') { - response.writeHead(200, { 'content-type': 'text/plain' }); - response.end('redirected'); - } else if (request.url == '/') { - response.writeHead(301, { - 'location': host + '/' + (request.headers['x-redirects'] ? '1' : 'redirected') - }); - response.end('redirect'); - } else { - var count = parseInt(request.url.substr(1)); - var max = parseInt(request.headers['x-redirects']); - response.writeHead(count < max ? 301 : 200, { - 'location': host + '/' + (count + 1) - }); - response.end(count.toString(10)); - } -} - -module.exports['Redirect'] = { - - setUp: setup(redirectResponse), - tearDown: teardown(), - - 'Should follow redirects': function(test) { - rest.get(host).on('complete', function(data) { - test.equal(data, 'redirected', 'returned: ' + p(data)); - test.done(); - }); - }, - - 'Should follow multiple redirects': function(test) { - rest.get(host, { - headers: { 'x-redirects': '5' } - }).on('complete', function(data) { - test.equal(data, '5', 'returned: ' + p(data)); - test.done(); - }); - } - -}; - - -function contentLengthResponse(request, response) { - response.writeHead(200, { 'content-type': 'text/plain' }); - if ('content-length' in request.headers) { - response.write(request.headers['content-length']); - } else { - response.write('content-length is not set'); - } - response.end(); -} - -module.exports['Content-Length'] = { - - setUp: setup(contentLengthResponse), - tearDown: teardown(), - - 'JSON content length': function(test) { - rest.post(host, { - data: JSON.stringify({ greeting: 'hello world' }) - }).on('complete', function(data) { - test.equal(26, data, 'should set content-length'); - test.done(); - }); - }, - - 'JSON multibyte content length': function (test) { - rest.post(host, { - data: JSON.stringify({ greeting: 'こんにちは世界' }) - }).on('complete', function(data) { - test.equal(36, data, 'should byte-size content-length'); - test.done(); - }); - } - -}; diff --git a/package.json b/package.json index 1aa4fce..9a3d89c 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,4 @@ -{ +{ "name": "faceplate", "version": "0.5.0", "author": { From d1bf99e4db044b5637a63db66cb10312c8531e0a Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Mon, 13 May 2013 14:34:29 -0400 Subject: [PATCH 11/16] added gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44f7ca4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +./node_modules/* From 33559bc0a3c0a701807576d00f0dd26ce207a808 Mon Sep 17 00:00:00 2001 From: Chris Tarquini Date: Mon, 13 May 2013 14:47:08 -0400 Subject: [PATCH 12/16] updated package.json --- package.json | 12 +++--------- strategy.js | 0 2 files changed, 3 insertions(+), 9 deletions(-) create mode 100644 strategy.js diff --git a/package.json b/package.json index 9a3d89c..1fcf99d 100644 --- a/package.json +++ b/package.json @@ -4,22 +4,16 @@ "author": { "name": "Heroku" }, - "homepage": "https://github.com/heroku/faceplate", + "homepage": "https://github.com/IlskenLabs/faceplate", "description": "Wrapper for Facebook authentication and API", "repository": { "type": "git", - "url": "https://github.com/heroku/faceplate" + "url": "https://github.com/IlskenLabs/faceplate" }, "dependencies": { "b64url": "1.0.3", - "restler": "2.0.0" + "restler": "~2.0.*" }, "readme": "# faceplate\n\nA Node.js wrapper for Facebook authentication and API\n\n## Usage\n\nUse as a connect middleware\n\n```javascript\n// create an express webserver\nvar app = require('express').createServer(\n express.bodyParser(),\n express.cookieParser(),\n require('faceplate').middleware({\n app_id: process.env.FACEBOOK_APP_ID,\n secret: process.env.FACEBOOK_SECRET,\n scope: 'user_likes,user_photos,user_photo_video_tags'\n })\n);\n\n// show friends\napp.get('/friends', function(req, res) {\n req.facebook.get('/me/friends', { limit: 4 }, function(friends) {\n res.send('friends: ' + require('util').inspect(friends));\n });\n});\n\n// use fql to show my friends using this app\napp.get('/friends_using_app', function(req, res) {\n req.facebook.fql('SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1', function(friends_using_app) {\n res.send('friends using app: ' + require('util').inspect(friends_using_app));\n });\n});\n\n// perform multiple fql queries at once\napp.get('/multiquery', function(req, res) {\n req.facebook.fql({\n likes: 'SELECT user_id, object_id, post_id FROM like WHERE user_id=me()',\n albums: 'SELECT object_id, cover_object_id, name FROM album WHERE owner=me()',\n },\n function(result) {\n var inspect = require('util').inspect;\n res.send('Yor likes: ' + inspect(result.likes) + ', your albums: ' + inspect(result.albums) );\n });\n});\n\n// See the full signed request details\napp.get('/signed_request', function(req, res) {\n res.send('Signed Request details: ' + require('util').inspect(req.facebook.signed_request));\n});\n\n```\n\n## License\n\nMIT\n", "readmeFilename": "README.md", - "_id": "faceplate@0.5.0", - "dist": { - "shasum": "91c8fb217dd130bb88ac343a0aaeac27c720bb81" - }, - "_from": "faceplate@0.5.0", - "_resolved": "https://registry.npmjs.org/faceplate/-/faceplate-0.5.0.tgz" } diff --git a/strategy.js b/strategy.js new file mode 100644 index 0000000..e69de29 From 421bfc2e5ce8d531475d491724153d240835bb93 Mon Sep 17 00:00:00 2001 From: IlskenLabs Date: Mon, 13 May 2013 16:12:39 -0300 Subject: [PATCH 13/16] changed repo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fcf99d..6d6044d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "description": "Wrapper for Facebook authentication and API", "repository": { "type": "git", - "url": "https://github.com/IlskenLabs/faceplate" + "url": "git@github.com:IlskenLabs/faceplate.git" }, "dependencies": { "b64url": "1.0.3", From 30709cf54f29b1123789475f96c305bc1d5136ad Mon Sep 17 00:00:00 2001 From: IlskenLabs Date: Mon, 13 May 2013 15:28:37 -0400 Subject: [PATCH 14/16] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d6044d..8bad8f9 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,5 @@ "restler": "~2.0.*" }, "readme": "# faceplate\n\nA Node.js wrapper for Facebook authentication and API\n\n## Usage\n\nUse as a connect middleware\n\n```javascript\n// create an express webserver\nvar app = require('express').createServer(\n express.bodyParser(),\n express.cookieParser(),\n require('faceplate').middleware({\n app_id: process.env.FACEBOOK_APP_ID,\n secret: process.env.FACEBOOK_SECRET,\n scope: 'user_likes,user_photos,user_photo_video_tags'\n })\n);\n\n// show friends\napp.get('/friends', function(req, res) {\n req.facebook.get('/me/friends', { limit: 4 }, function(friends) {\n res.send('friends: ' + require('util').inspect(friends));\n });\n});\n\n// use fql to show my friends using this app\napp.get('/friends_using_app', function(req, res) {\n req.facebook.fql('SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1', function(friends_using_app) {\n res.send('friends using app: ' + require('util').inspect(friends_using_app));\n });\n});\n\n// perform multiple fql queries at once\napp.get('/multiquery', function(req, res) {\n req.facebook.fql({\n likes: 'SELECT user_id, object_id, post_id FROM like WHERE user_id=me()',\n albums: 'SELECT object_id, cover_object_id, name FROM album WHERE owner=me()',\n },\n function(result) {\n var inspect = require('util').inspect;\n res.send('Yor likes: ' + inspect(result.likes) + ', your albums: ' + inspect(result.albums) );\n });\n});\n\n// See the full signed request details\napp.get('/signed_request', function(req, res) {\n res.send('Signed Request details: ' + require('util').inspect(req.facebook.signed_request));\n});\n\n```\n\n## License\n\nMIT\n", - "readmeFilename": "README.md", + "readmeFilename": "README.md" } From 2e64ed2762d33c8878bedf45ce221512e0bafc38 Mon Sep 17 00:00:00 2001 From: IlskenLabs Date: Mon, 13 May 2013 16:29:05 -0300 Subject: [PATCH 15/16] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bad8f9..c108218 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "b64url": "1.0.3", - "restler": "~2.0.*" + "restler": "~2.0.0" }, "readme": "# faceplate\n\nA Node.js wrapper for Facebook authentication and API\n\n## Usage\n\nUse as a connect middleware\n\n```javascript\n// create an express webserver\nvar app = require('express').createServer(\n express.bodyParser(),\n express.cookieParser(),\n require('faceplate').middleware({\n app_id: process.env.FACEBOOK_APP_ID,\n secret: process.env.FACEBOOK_SECRET,\n scope: 'user_likes,user_photos,user_photo_video_tags'\n })\n);\n\n// show friends\napp.get('/friends', function(req, res) {\n req.facebook.get('/me/friends', { limit: 4 }, function(friends) {\n res.send('friends: ' + require('util').inspect(friends));\n });\n});\n\n// use fql to show my friends using this app\napp.get('/friends_using_app', function(req, res) {\n req.facebook.fql('SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1', function(friends_using_app) {\n res.send('friends using app: ' + require('util').inspect(friends_using_app));\n });\n});\n\n// perform multiple fql queries at once\napp.get('/multiquery', function(req, res) {\n req.facebook.fql({\n likes: 'SELECT user_id, object_id, post_id FROM like WHERE user_id=me()',\n albums: 'SELECT object_id, cover_object_id, name FROM album WHERE owner=me()',\n },\n function(result) {\n var inspect = require('util').inspect;\n res.send('Yor likes: ' + inspect(result.likes) + ', your albums: ' + inspect(result.albums) );\n });\n});\n\n// See the full signed request details\napp.get('/signed_request', function(req, res) {\n res.send('Signed Request details: ' + require('util').inspect(req.facebook.signed_request));\n});\n\n```\n\n## License\n\nMIT\n", "readmeFilename": "README.md" From 4c05224adfc9e4fa0e1b72264027ef525da12bff Mon Sep 17 00:00:00 2001 From: IlskenLabs Date: Mon, 13 May 2013 16:30:28 -0300 Subject: [PATCH 16/16] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c108218..8dce5bc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "description": "Wrapper for Facebook authentication and API", "repository": { "type": "git", - "url": "git@github.com:IlskenLabs/faceplate.git" + "url": "https://github.com/IlskenLabs/faceplate.git" }, "dependencies": { "b64url": "1.0.3",