rest-methods
Version:
Declaratively publish functions for remote invocation.
360 lines (301 loc) • 12.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj["default"] = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _lodash = require("lodash");
var _lodash2 = _interopRequireDefault(_lodash);
var _bluebird = require("bluebird");
var _bluebird2 = _interopRequireDefault(_bluebird);
var _ServerMethod = require("./ServerMethod");
var _ServerMethod2 = _interopRequireDefault(_ServerMethod);
var _middleware = require("./middleware");
var _middleware2 = _interopRequireDefault(_middleware);
var _connect = require("connect");
var _connect2 = _interopRequireDefault(_connect);
var _pageJs = require("../page-js");
var _pageJs2 = _interopRequireDefault(_pageJs);
var _const = require("../const");
var _errors = require("../errors");
var _http = require("http");
var _http2 = _interopRequireDefault(_http);
var _jsUtil = require("js-util");
var util = _interopRequireWildcard(_jsUtil);
var _url = require("../url");
/**
* Generates a standard URL for a method.
*
* @param basePath: The base path to prefix the URL with.
* @param path: The main part of the URL.
*
* @return string.
*/
var methodUrl = function methodUrl(basePath, path) {
path = path.replace(/^\/*/, "");
var url = basePath + "/" + path;
url = "/" + url.replace(/^\/*/, "");
return url;
};
/**
* Represents a REST API server.
*/
var Server = (function () {
/**
* Constructor
*
* @param connect: The connect app to apply the middleware to.
* @param options:
* - name: The name of the service.
* - basePath: The base path to prepend URL"s with.
* - version: The version number of the service.
* - docs: Flag indicating if generated docs should be serverd.
* Default: true.
*/
function Server() {
var _this = this;
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
_classCallCheck(this, Server);
// Store state.
var self = this;
this.name = options.name || "Server Methods";
this.version = options.version || "0.0.0";
this.docs = _lodash2["default"].isBoolean(options.docs) ? options.docs : true;
this.middleware = (0, _middleware2["default"])(this);
// Private state.
this[_const.METHODS] = {};
this[_const.HANDLERS] = {
before: new util.Handlers(),
after: new util.Handlers()
};
// Store base path.
var path = options.basePath;
if (_lodash2["default"].isString(path)) {
path = path.replace(/^\/*/, "").replace(/\/*$/, "");
} else {
path = "";
}
this.basePath = "/" + path;
/**
* Registers or retrieves the complete set of methods.
*
* @param definition: An object containing the method definitions.
* @return an object containing the set of method definitions.
*/
this.methods = function (definition) {
// Write: store method definitions if passed.
if (definition) {
(function () {
var createUrl = function createUrl(urlPath, methodDef) {
return methodUrl(_this.basePath, methodDef.url || urlPath);
};
Object.keys(definition).forEach(function (key) {
var methods = _this[_const.METHODS];
if (methods[key]) {
throw new Error("Method \"" + key + "\" already exists.");
}
var value = definition[key];
var url = createUrl(key, value);
var methodSet = undefined;
if (_lodash2["default"].isFunction(value)) {
// A single function was provided.
// Use it for all the HTTP verbs.
var func = value;
methodSet = {
get: new _ServerMethod2["default"](key, func, url, "GET"),
put: new _ServerMethod2["default"](key, func, url, "PUT"),
post: new _ServerMethod2["default"](key, func, url, "POST"),
"delete": new _ServerMethod2["default"](key, func, url, "DELETE")
};
} else if (_lodash2["default"].isObject(value)) {
// Create individual methods for each verb.
methodSet = {};
if (value.get) {
methodSet.get = new _ServerMethod2["default"](key, value.get, url, "GET", value.docs);
}
if (value.put) {
methodSet.put = new _ServerMethod2["default"](key, value.put, url, "PUT", value.docs);
}
if (value.post) {
methodSet.post = new _ServerMethod2["default"](key, value.post, url, "POST", value.docs);
}
if (value["delete"]) {
methodSet["delete"] = new _ServerMethod2["default"](key, value["delete"], url, "DELETE", value.docs);
}
} else {
throw new Error("Type of value for method \"" + key + "\" not supported. Must be function or object.");
}
// Store an pointer to the method.
// NOTE: This allows the server and client to behave isomorphically.
// Server code can call the methods (directly) using the same
// pathing/namespace object that the client uses, for example:
//
// server.methods.foo.put(123, "hello");
//
var stub = util.ns(_this.methods, key, { delimiter: "/" });
["get", "put", "post", "delete"].forEach(function (verb) {
var method = methodSet[verb];
if (method) {
stub[verb] = function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
// Prepare the URL for the method.
var route = method.route;
var totalUrlParams = route.keys.length;
var invokeUrl = (0, _url.getMethodUrl)(method.name, null, route, args);
if (totalUrlParams > 0) {
args = _lodash2["default"].clone(args);
args.splice(0, totalUrlParams);
}
// Invoke the method.
return self[_const.INVOKE](method, args, invokeUrl);
};
}
});
// Store the values.
_this[_const.METHODS][key] = methodSet;
});
})();
}
// Read.
return _this[_const.METHODS];
};
// Finish up (Constructor).
return this;
}
// ----------------------------------------------------------------------------
/**
* Determines whether the given URL path matches any of
* the method routes.
* @param url: {string} The URL path to match.
* @param verb: {string} The HTTP verb to match (GET|PUT|POST|DELETE).
* @return {ServerMethod}
*/
_createClass(Server, [{
key: "match",
value: function match(url, verb) {
verb = verb.toLowerCase();
var context = new _pageJs2["default"].Context(url);
var methods = this[_const.METHODS];
var methodNames = Object.keys(methods);
if (!_lodash2["default"].isEmpty(methodNames)) {
var methodName = _lodash2["default"].find(Object.keys(methods), function (key) {
var methodVerb = methods[key][verb];
var isMatch = methodVerb && methodVerb.pathRoute.match(context.path, context.params);
return isMatch;
});
var method = methods[methodName];
}
return method ? method[verb] : undefined;
}
/**
* Registers a handler to invoke BEFORE a server method is invoked.
* @param {Function} func(e): The function to invoke.
*/
}, {
key: "before",
value: function before(func) {
this[_const.HANDLERS].before.push(func);
return this;
}
/**
* Registers a handler to invoke AFTER a server method is invoked.
* @param {Function} func(e): The function to invoke.
*/
}, {
key: "after",
value: function after(func) {
this[_const.HANDLERS].after.push(func);
return this;
}
/**
* Private: Invokes the specified method with BEFORE/AFTER handlers.
*/
}, {
key: _const.INVOKE,
value: function value(method, args, url) {
var _this2 = this;
if (args === undefined) args = [];
return new _bluebird2["default"](function (resolve, reject) {
var startedAt = new Date();
var beforeArgs = {
args: args,
url: url,
verb: method.verb,
name: method.name,
"throw": function _throw(status, message) {
throw new _errors.ServerMethodError(status, method.name, args, message);
}
};
// BEFORE/AFTER handlers.
var invokeHandlers = function invokeHandlers(handlers, e) {
handlers.context = e;
handlers.invoke(e);
};
var invokeAfterHandlers = function invokeAfterHandlers(err, result) {
var afterArgs = _lodash2["default"].clone(beforeArgs);
afterArgs.result = result;
afterArgs.error = err;
afterArgs.msecs = new Date() - startedAt;
delete afterArgs["throw"]; // Cannot throw after the method has been invoked.
invokeHandlers(_this2[_const.HANDLERS].after, afterArgs);
};
invokeHandlers(_this2[_const.HANDLERS].before, beforeArgs);
// Pass execution to the method.
method.invoke(args, url).then(function (result) {
resolve(result);
invokeAfterHandlers(undefined, result);
})["catch"](function (err) {
reject(err);
invokeAfterHandlers(err, undefined);
});
});
}
/**
* Starts the server.
* Only use this if you"re not passing in a connect server that
* you are otherwise starting/managing independely for other purposes.
* @param options:
* - port: The HTTP port to use.
* - silent: Flag indicating if logging should be suppressed.
* Default: false
*
* @return
*/
}, {
key: "start",
value: function start() {
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var PORT = options.port || 3030;
var SILENT = options.silent || false;
// Start the server.
var app = (0, _connect2["default"])().use(this.middleware);
_http2["default"].createServer(app).listen(PORT);
// Output some helpful details to the console.
if (SILENT !== true) {
var HR = _lodash2["default"].repeat("-", 80);
var ADDRESS = "localhost:" + PORT;
if (!this.basePath !== "/") {
ADDRESS += this.basePath;
}
console.log("");
console.log(HR);
console.log(" Started: ", this.name);
console.log(" - version: ", this.version);
console.log(" - address: ", ADDRESS);
console.log(HR);
console.log("");
}
// Finish up.
return this;
}
}]);
return Server;
})();
exports["default"] = function (options) {
return new Server(options);
};
module.exports = exports["default"];