Skip to content
This repository has been archived by the owner on Jan 19, 2024. It is now read-only.

Implement promises alongside callbacks #53

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
"strict": true,
"unused": true,
"globals": {
"require": true,
"Promise": true,
"after": true,
"afterEach": true,
"before": true,
"beforeEach": true,
"after": true,
"define": true,
"describe": true,
"it": true,
"expect": true,
"Promise": true
"it": true,
"require": true
}
}
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ npm install fuel-rest --save
* `options.auth` - will be passed into [getAccessToken][4] inside Fuel Auth
* `options.uri` - can either be a full url or a path that is appended to `options.origin` used at initialization ([url.resolve][2])
* `options.retry` - boolean value representing whether or not to retry request (and request new token) on 401 invalid token response. `default: false`
* `callback` - executed after task is completed. **required**
* `callback` - executed after task is completed.
* if no callback is passed, you'll need to use the promise interface
* **get | post | put | patch | delete(options, callback)**
* `options` - see apiRequest options
* `options.retry` - see above for description. `default: true`
Expand Down Expand Up @@ -75,6 +76,21 @@ RestClient.get(options, function(err, response) {
// response.res === full response from request client
console.log(response);
});

// or with promises
RestClient
.get(options)
.then(function(response) {
// will be delivered with 200, 400, 401, 500, etc status codes
// response.body === payload from response
// response.res === full response from request client
console.log(response);
})
.catch(function(err) {
// error here
console.log(err);
});
});
```

## Contributors
Expand Down
154 changes: 80 additions & 74 deletions lib/fuel-rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var helpers = require('./helpers');
var request = require('request');
var _ = require('lodash');
var FuelAuth = require('fuel-auth');
var Promiser = (typeof Promise === 'undefined') ? require('bluebird') : Promise;

var FuelRest = function(options) {
'use strict';
Expand Down Expand Up @@ -61,81 +62,86 @@ var FuelRest = function(options) {
FuelRest.prototype.apiRequest = function(options, callback) {
'use strict';

// we need a callback
if(!_.isFunction(callback)) {
throw new TypeError('callback argument is required');
}
var self = this;

// we need options
if(!_.isPlainObject(options)) {
throw new TypeError('options argument is required');
}

this.AuthClient.getAccessToken(_.clone(options.auth), function(err, body) {
var localError, retry, authOptions;

if(err) {
helpers.respond('error', err, callback, 'FuelAuth');
return;
}

// if there's no access token we have a problem
if(!body.accessToken) {
localError = new Error('No access token');
localError.res = body;
helpers.respond('error', localError, callback, 'FuelAuth');
return;
}

// retry request?
retry = options.retry || false;
authOptions = _.clone(options.auth);

// clean up
delete options.retry;
delete options.auth;

// if we don't have a fully qualified URL let's make one
options.uri = helpers.resolveUri(this.origin, options.uri);

// merge headers
options.headers = _.merge({}, this.defaultHeaders, options.headers);

// adding the bearer token
options.headers.Authorization = options.headers.Authorization || 'Bearer ' + body.accessToken;

// send request to api
request(options, function(err, res, body) {
var parsedBody;

if(err) {
helpers.respond('error', err, callback, 'Request Module inside apiRequest');
return;
}

// check if we should retry req
if(helpers.check401header(res) && !!retry) {
options.auth = authOptions;
this.apiRequest(options, callback);
return;
}

// checking to make sure it's json from api
if(!res.headers['content-type'] || res.headers['content-type'].split(';')[0].toLowerCase() !== 'application/json') {
helpers.respond('error', new Error('API did not return JSON'), callback, 'Fuel REST');
return;
}

// trying to parse body
try {
parsedBody = JSON.parse(body);
} catch(err) {
parsedBody = body;
}

helpers.respond('response', { res: res, body: parsedBody }, callback);
}.bind(this));
}.bind(this));
return self.AuthClient
.getAccessToken(_.clone(options.auth))
.then(function(authResponse) {
return new Promiser(function(resolve, reject) {
var authOptions;
var jsonRequested;
var localError;
var retry;

if(!authResponse.accessToken) {
localError = new Error('No access token');
localError.res = authResponse;
reject(localError);
return;
}

// retry request?
retry = options.retry || false;
authOptions = _.clone(options.auth);

// clean up
delete options.retry;
delete options.auth;

options.uri = helpers.resolveUri(self.origin, options.uri);
options.headers = _.merge({}, self.defaultHeaders, options.headers);
options.headers.Authorization = options.headers.Authorization || 'Bearer ' + authResponse.accessToken;

request(options, function(err, res, body) {
var parsedBody, restResponse;

if(err) {
reject(err);
return;
}

// check if we should retry req
if(helpers.isValid401(res) && retry) {
options.auth = authOptions;
self.apiRequest(options, callback);
return;
}

// checking to make sure it's json from api
jsonRequested = res.headers['content-type'] && res.headers[ 'content-type'].split(';')[ 0].toLowerCase() === 'application/json';
if(!jsonRequested) {
localError = new Error('API did not return JSON');
helpers.cbRespond('error', localError, callback);
reject(err);
return;
}

// trying to parse body
try {
parsedBody = JSON.parse(body);
} catch(err) {
parsedBody = body;
}

restResponse = {
res: res
, body: parsedBody
};

helpers.cbRespond('response', restResponse, callback);
resolve(restResponse);
});
});
})
.catch(function(err) {
helpers.cbRespond('error', err, callback);
return err;
});
};

FuelRest.prototype.get = function(options, callback) {
Expand All @@ -144,7 +150,7 @@ FuelRest.prototype.get = function(options, callback) {
options.method = 'GET';
options.retry = true;

this.apiRequest(options, callback);
return this.apiRequest(options, callback);
};

FuelRest.prototype.post = function(options, callback) {
Expand All @@ -153,7 +159,7 @@ FuelRest.prototype.post = function(options, callback) {
options.method = 'POST';
options.retry = true;

this.apiRequest(options, callback);
return this.apiRequest(options, callback);
};

FuelRest.prototype.put = function(options, callback) {
Expand All @@ -162,7 +168,7 @@ FuelRest.prototype.put = function(options, callback) {
options.method = 'PUT';
options.retry = true;

this.apiRequest(options, callback);
return this.apiRequest(options, callback);
};

FuelRest.prototype.patch = function(options, callback) {
Expand All @@ -171,7 +177,7 @@ FuelRest.prototype.patch = function(options, callback) {
options.method = 'PATCH';
options.retry = true;

this.apiRequest(options, callback);
return this.apiRequest(options, callback);
};

FuelRest.prototype.delete = function(options, callback) {
Expand All @@ -180,7 +186,7 @@ FuelRest.prototype.delete = function(options, callback) {
options.method = 'DELETE';
options.retry = true;

this.apiRequest(options, callback);
return this.apiRequest(options, callback);
};

module.exports = FuelRest;
37 changes: 23 additions & 14 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
'use strict';

var _ = require('lodash');
var url = require('url');

var invalidTypeMsg = 'invalid response type';

module.exports = {
check401header: function(res) {
var is401 = res.statusCode === 401;
var isFailureFromBadToken = /^Bearer\s.+?invalid_token/.test(res.headers['www-authenticate']);
isValid401: function(res) {
var is401 = (res.statusCode === 401);
var isFailureFromBadToken = false;

if(res.headers && res.headers['www-authenticate']) {
isFailureFromBadToken = /^Bearer\s.+?invalid_token/.test(res.headers['www-authenticate']);
}

return is401 && isFailureFromBadToken;
}
, respond: function(type, data, callback, errorFrom) {
, resolveUri: function(origin, uri) {
if(origin && uri && !/^http/.test(uri)) {
uri = url.resolve(origin, uri);
}
return uri;
}
, cbRespond: function(type, data, callback) {
if(!_.isFunction(callback)) {
return;
}

// if it's an error and we have where it occured, let's tack it on
if(type === 'error') {
if(!!errorFrom) {
data.errorPropagatedFrom = errorFrom;
}

callback(data, null);
} else if(type === 'response') {
callback(null, data);
} else {
callback(invalidTypeMsg, null);
}
}
, resolveUri: function(origin, uri) {
if(!/^http/.test(uri)) {
uri = url.resolve(origin, uri);
}
return uri;
}
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
"grunt": "~0.4.5",
"grunt-bump": "~0.3.0",
"grunt-contrib-jshint": "~0.11.0",
"grunt-jscs": "^1.8.0",
"grunt-jscs": "~1.8.0",
"mocha": "~2.2.1",
"promise": "~6.1.0",
"sinon": "~1.13.0"
},
"dependencies": {
"bluebird": "~2.9.25",
"fuel-auth": "~1.0.0",
"lodash": "~3.5.0",
"request": "~2.53.0"
Expand Down
2 changes: 1 addition & 1 deletion test/mock-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ module.exports = function(port) {
if(reqUrl === validUrls.invalidToken) {
if(totalRequests === 0) {
res.writeHead(401, {
'WWW-Authenticate': 'Bearer realm="example.com", error="invalid_token", error_description="The access token expired"'
'WWW-Authenticate': sampleResponses.invalidToken
});
res.end(JSON.stringify(sampleResponses['401']));
} else {
Expand Down
1 change: 1 addition & 0 deletions test/sample-responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ module.exports = {
, errorcode: 1
, message: "Unauthorized"
}
, invalidToken: 'Bearer realm="example.com", error="invalid_token", error_description="The access token expired"'
};
Loading