diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..261697e --- /dev/null +++ b/index.d.ts @@ -0,0 +1,148 @@ +// Type definitions for restler-q +// Adapted from restler definitions by Cyril Schumacher + +/// + +declare module "@gradecam/restler-q" { + import * as http from "http"; + import * as Q from "q"; + + interface RestlerMethods { + del(url: string, options?: Object): Q.Promise; + get(url: string, options?: RestlerOptions): Q.Promise; + head(url: string, options?: RestlerOptions): Q.Promise; + /** Send json data via GET method. */ + json(url: string, data?: any, options?: RestlerOptions, method?: string): Q.Promise; + patch(url: string, options?: RestlerOptions): Q.Promise; + patchJson(url: string, data?: any, options?: RestlerOptions): Q.Promise; + post(url: string, options?: RestlerOptions): Q.Promise; + postJson(url: string, data?: any, options?: RestlerOptions): Q.Promise; + put(url: string, options?: RestlerOptions): Q.Promise; + putJson(url: string, data?: any, options?: RestlerOptions): Q.Promise; + service(url: string, options?: RestlerOptions): Q.Promise; + } + + interface RestlerStatic extends RestlerMethods { + 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 multipart/form-encoded. + * @type {boolean} + */ + multipart?: boolean; + + /** + * A function that will be called on the returned data. Use any of predefined restler.parsers. + * @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; +} diff --git a/index.js b/index.js index 73d8415..b91314f 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ /* jshint node:true, unused:true */ "use strict"; -module.exports = require('./lib/restler-q'); \ No newline at end of file +module.exports = require('./lib/restler-q'); diff --git a/lib/restler-q.js b/lib/restler-q.js index 431cb51..5540379 100644 --- a/lib/restler-q.js +++ b/lib/restler-q.js @@ -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; +} diff --git a/package.json b/package.json index 1649b0e..54b6af1 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } }