Skip to content

Add support for Service and properly handle timeouts #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
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
148 changes: 148 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Type definitions for restler-q
// Adapted from restler definitions by Cyril Schumacher <https://github.com/cyrilschumacher>

/// <reference types="node"/>

declare module "@gradecam/restler-q" {
import * as http from "http";
import * as Q from "q";

interface RestlerMethods<T> {
del(url: string, options?: Object): Q.Promise<T>;
get(url: string, options?: RestlerOptions): Q.Promise<T>;
head(url: string, options?: RestlerOptions): Q.Promise<T>;
/** Send json data via GET method. */
json(url: string, data?: any, options?: RestlerOptions, method?: string): Q.Promise<T>;
patch(url: string, options?: RestlerOptions): Q.Promise<T>;
patchJson(url: string, data?: any, options?: RestlerOptions): Q.Promise<T>;
post(url: string, options?: RestlerOptions): Q.Promise<T>;
postJson(url: string, data?: any, options?: RestlerOptions): Q.Promise<T>;
put(url: string, options?: RestlerOptions): Q.Promise<T>;
putJson(url: string, data?: any, options?: RestlerOptions): Q.Promise<T>;
service(url: string, options?: RestlerOptions): Q.Promise<T>;
}

interface RestlerStatic extends RestlerMethods<any> {
spread: RestlerMethods<[any, http.IncomingMessage]>;
}

/**
* Interface for the header.
* @interface
*/
interface RestlerOptionsHeader {
[headerName: string]: string;
}

/**
* Interface for restler options.
* @interface
*/
interface RestlerOptions {
/**
* OAuth Bearer Token.
* @type {string}
*/
accessToken?: string;

/**
* HTTP Agent instance to use. If not defined globalAgent will be used. If false opts out of connection pooling with an Agent, defaults request to Connection: close.
* @type {any}
*/
agent?: any;

/**
* A http.Client instance if you want to reuse or implement some kind of connection pooling.
* @type {any}
*/
client?: any;

/**
* Data to be added to the body of the request.
* @type {any}
*/
data?: any;

/**
* Encoding of the response body
* @type {string}
*/
decoding?: string;

/**
* Encoding of the request body.
* @type {string}
*/
encoding?: string;

/**
* If set will recursively follow redirects.
* @type {boolean}
*/
followRedirects?: boolean;

/**
* A hash of HTTP headers to be sent.
* @type {RestlerOptionsHeader}
*/
headers?: RestlerOptionsHeader;

/**
* Request method
* @type {string}
*/
method?: string;

/**
* If set the data passed will be formatted as <code>multipart/form-encoded</code>.
* @type {boolean}
*/
multipart?: boolean;

/**
* A function that will be called on the returned data. Use any of predefined <code>restler.parsers</code>.
* @type {any}
*/
parser?: any;

/**
* Basic auth password.
* @type {string}
*/
password?: string;

/**
* Query string variables as a javascript object, will override the querystring in the URL.
* @type {any}
*/
query?: any;

/**
* If true, the server certificate is verified against the list of supplied CAs.
* An 'error' event is emitted if verification fails. Verification happens at the connection level, before the HTTP request is sent.
* @type {boolean}
*/
rejectUnauthorized?: boolean;

/**
* Emit the timeout event when the response does not return within the said value (in ms).
* @type {number}
*/
timeout?: number;

/**
* Basic auth username.
* @type {string}
*/
username?: string;

/**
* Options for xml2js.
* @type {any}
*/
xml2js?: any;
}

let restler: RestlerStatic;
export = restler;
}
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* jshint node:true, unused:true */
"use strict";

module.exports = require('./lib/restler-q');
module.exports = require('./lib/restler-q');
174 changes: 143 additions & 31 deletions lib/restler-q.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,165 @@
/* jshint node:true, unused:true */
"use strict";

var rest = require('restler');
var url = require('url');

var rest = require('@gradecam/restler');
var Q = require('q');

var restler_q = ['request', 'get','patch','post','put','del','head','json','patchJson','postJson','putJson'].reduce(function(memo, method) {
var underlying = rest[method];
if(underlying) {
memo[method] = wrapMethod(underlying);
memo.spread[method] = wrapMethod(underlying, true);
}
return memo;
}, {
spread: {},
Request: rest.Request,
parsers: rest.parsers,
file: rest.file,
data: rest.data,
Service: Service,
service: service
});

module.exports = restler_q;

var STATUS_CODES = {
'Unauthorized': 401,
'Forbidden': 403,
'Not Found': 404,
'Internal Server Error': 500,
'Not Implemented': 501,
'Service Unavailable': 503,
};

function str2error(str, req) {
var err = new Error(str);
err.code = STATUS_CODES[str];
err.statusCode = err.code;
if (req) { err.url = url.format(req.url); }
return err;
}

function wrap(r, spread) {
var defer = Q.defer();
var defer = Q.defer();

r.on('success', function(result, response) {
if(spread) return defer.resolve([ result, response ]);
r.on('success', function(result, response) {
if(spread) { return defer.resolve([ result, response ]); }
defer.resolve(result);
});

defer.resolve(result);
});
r.on('timeout', function(ms) {
var err = new Error('ETIMEDOUT');
err.code = 'ETIMEDOUT';
err.errno = 'ETIMEDOUT';
err.statusCode = 408;
err.syscall = 'connect';
err.timeout = ms;
err.url = url.format(r.url);
defer.reject(err);
});

r.on('fail', function(err, response) {
if(spread) err.response = response;
r.on('fail', function(err, response) {
if (typeof(err) === 'string') { err = str2error(err, r); }
if(spread) { err.response = response; }

defer.reject(err);
});
if (!err.url) { err.url = url.format(r.url); }
defer.reject(err);
});

r.on('error', function(err, response) {
if(spread) err.response = response;
r.on('error', function(err, response) {
if (typeof(err) === 'string') { err = str2error(err, r); }
if(spread) { err.response = response; }

defer.reject(err);
});
if (!err.url) { err.url = url.format(r.url); }
defer.reject(err);
});

r.on('abort', function() {
defer.reject(new Error('Operation aborted'));
});
r.on('abort', function() {
var err = new Error('Operation aborted');
err.url = url.format(r.url);
defer.reject(err);
});

return defer.promise;
return defer.promise;
}

function wrapMethod(method, spread) {
return function() {
var request = method.apply(rest, arguments);
return wrap(request, spread);
};
return function() {
var request = method.apply(rest, arguments);
return wrap(request, spread);
};
}

function mixin(target, source) {
source = source || {};
Object.keys(source).forEach(function(key) {
target[key] = source[key];
});

module.exports = ['get','post','put','del','head', 'json', 'postJson'].reduce(function(memo, method) {
var underlying = rest[method];
if(underlying) {
memo[method] = wrapMethod(underlying);
memo.spread[method] = wrapMethod(underlying, true);
}
return memo;
}, {
spread: {}
return target;
}

function Service(defaults) {
defaults = defaults || {};
if (defaults.baseURL) {
this.baseURL = defaults.baseURL;
delete defaults.baseURL;
}
this.defaults = defaults;
}

var url = require('url');

mixin(Service.prototype, {
request: function(path, data, options) {
return restler_q.request(this._url(path), data, this._withDefaults(options));
},
get: function(path, options) {
return restler_q.get(this._url(path), this._withDefaults(options));
},
head: function(path, options) {
return restler_q.head(this._url(path), this._withDefaults(options));
},
patch: function(path, data, options) {
return restler_q.patch(this._url(path), data, this._withDefaults(options));
},
put: function(path, data, options) {
return restler_q.put(this._url(path), data, this._withDefaults(options));
},
post: function(path, data, options) {
return restler_q.post(this._url(path), data, this._withDefaults(options));
},
json: function(method, path, data, options) {
return restler_q.json(this._url(path), data, this._withDefaults(options), method);
},
postJson: function(path, data, options) {
return restler_q.postJson(this._url(path), data, this._withDefaults(options));
},
putJson: function(path, data, options) {
return restler_q.putJson(this._url(path), data, this._withDefaults(options));
},
del: function(path, options) {
return restler_q.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 opts = mixin({}, this.defaults);
return mixin(opts, options);
}
});

function service(constructor, defaults, methods) {
constructor.prototype = new Service(defaults || {});
mixin(constructor.prototype, methods);
return constructor;
}
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "restler-q",
"version": "0.1.2",
"name": "@gradecam/restler-q",
"version": "0.2.0",
"description": "Q Wrapper for Restler",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "make test"
},
"repository": {
"type": "git",
"url": "https://github.com/troupe/restler-q"
"url": "https://github.com/gradecam/restler-q"
},
"keywords": [
"q",
Expand All @@ -17,15 +18,20 @@
"restler"
],
"author": "Andrew Newdigate",
"contributors": [
"Robert Porter",
"Jarom Loveridge"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/troupe/restler-q/issues"
"url": "https://github.com/gradecam/restler-q/issues"
},
"dependencies": {
"q": "^1.0.0",
"restler": "^3.0.0"
"@gradecam/restler": "^3.4.0",
"q": "^1.4.1"
},
"devDependencies": {
"mocha": "^2.0.0"
"@types/node": "^6.0.110",
"mocha": "^5.1.1"
}
}