UNPKG

rest-methods

Version:

Declaratively publish functions for remote invocation.

360 lines (301 loc) 12.4 kB
"use strict"; 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"];