UNPKG

forerunnerdb

Version:

A NoSQL document store database for browsers and Node.js.

971 lines (820 loc) 31.2 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: NodeApiServer.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: NodeApiServer.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>"use strict"; // NOTE: This class instantiates individually but shares a single // http server after listen() is called on any instance // Import external names locally var Shared = require('./Shared'), express = require('express'), bodyParser = require('body-parser'), cors = require('cors'), async = require('async'), fs = require('fs'), http = require('http'), https = require('https'), url = require('url'), pem = require('pem'), app = express(), server, Core, CoreInit, Db, NodeApiServer, ReactorIO, Overload, _access = {}, _accessOrder = [], _io = {}; /** * The NodeApiServer class. * @class * @constructor */ NodeApiServer = function () { this.init.apply(this, arguments); }; /** * The init method that can be overridden or extended. * @param {Core} core The ForerunnerDB core instance. */ NodeApiServer.prototype.init = function (core) { var self = this; self._core = core; self._app = app; this.name('ApiServer'); this.rootPath('/fdb'); }; Shared.addModule('NodeApiServer', NodeApiServer); Shared.mixin(NodeApiServer.prototype, 'Mixin.Common'); Shared.mixin(NodeApiServer.prototype, 'Mixin.Events'); Shared.mixin(NodeApiServer.prototype, 'Mixin.ChainReactor'); Core = Shared.modules.Core; CoreInit = Core.prototype.init; Db = Shared.modules.Db; ReactorIO = Shared.modules.ReactorIO; Overload = Shared.overload; Shared.synthesize(NodeApiServer.prototype, 'name'); Shared.synthesize(NodeApiServer.prototype, 'rootPath', function (val) { if (val !== undefined &amp;&amp; server) { throw('Cannot set rootPath of server after start() has been called!'); } if (val !== undefined &amp;&amp; !server) { this._rootSectionCount = (val.split('/').length - 1); if (this._rootSectionCount &lt; 0) { this._rootSectionCount = 0; } } return this.$super.call(this, val); }); /** * Starts the rest server listening for requests against the ip and * port number specified. * @param {String} host The IP address to listen on, set to 0.0.0.0 to * listen on all interfaces. * @param {String} port The port to listen on. * @param {Object} options An options object. * @param {Function=} callback The method to call when the server has * started (or failed to start). * @returns {NodeApiServer} */ NodeApiServer.prototype.start = function (host, port, options, callback) { var self = this; // Start listener if (!server) { this._startData = { host: host, port: port, options: options, callback: callback }; if (options &amp;&amp; options.cors === true) { // Enable cors middleware app.use(cors({origin: true})); // Allow preflight CORS app.options('*', cors({origin: true})); } // Parse body in requests app.use(bodyParser.json()); // Parse JSON as a query parameter app.use(function (req, res, next) { var query, jsonData, urlObj = url.parse(req.url); req.json = req.json || {}; if (urlObj.query) { try { // Try to parse the query parameter as URI encoded query = urlObj.query; query = decodeURIComponent(query); if (query) { // We got a query param, try to decode as JSON jsonData = JSON.parse(decodeURIComponent(query)); // Now copy any fields from jsonData to req.json Shared.mixin(req.json, jsonData); } } catch (e) { // Check for normal query params if (req.query &amp;&amp; Object.keys(req.query).length > 0) { return next(); } else { res.status(500).send('Error parsing query string ' + query + ' ' + e); return; } } } return next(); }); // Activate routes this._defineRoutes(); self._createHttpServer(options, function (err, httpServer) { if (!err) { server = httpServer.listen(port, host, function (err) { if (!err) { if (options &amp;&amp; options.ssl &amp;&amp; options.ssl.enable) { console.log('ForerunnerDB REST API listening at https://%s:%s/fdb', host, port); } else { console.log('ForerunnerDB REST API listening at http://%s:%s/fdb', host, port); } if (callback) { callback(false, server); } self.emit('started'); } else { console.log('Listen error', err); callback(err); } }); } }); } else { // Server already running if (callback) { callback(false, server); } } return this; }; /** * Gets the express app (router). * @returns {*} */ NodeApiServer.prototype.serverApp = function () { return app; }; /** * Gets the express library. * @returns {*|exports|module.exports} */ NodeApiServer.prototype.express = function () { return express; }; NodeApiServer.static = function (urlPath, folderPath) { return app.use(urlPath, express.static(folderPath)); }; /** * Creates the http(s) server instance. * @param options * @param callback * @private */ NodeApiServer.prototype._createHttpServer = function (options, callback) { var self = this, ssl, httpServer, i; if (options &amp;&amp; options.ssl &amp;&amp; options.ssl.enable) { // Check if we were provided certificate data if (options.ssl.key &amp;&amp; options.ssl.cert) { ssl = { key: fs.readFileSync(options.ssl.key), cert: fs.readFileSync(options.ssl.cert) }; if (options.ssl.ca) { ssl.ca = []; for (i = 0; i &lt; options.ssl.ca.length; i++) { ssl.ca.push(fs.readFileSync(options.ssl.ca[i])); } } httpServer = https.createServer(ssl, app); callback(false, httpServer); } else { // Check for existing certs we have already generated fs.stat('./forerunnerdbTempCert/privkey.pem', function (err, stat) { if (!err) { fs.stat('./forerunnerdbTempCert/fullchain.pem', function (err, stat) { if (!err) { ssl = { key: fs.readFileSync('./forerunnerdbTempCert/key.pem'), cert: fs.readFileSync('./forerunnerdbTempCert/cert.pem') }; httpServer = https.createServer(ssl, app); callback(false, httpServer); } else { self._generateSelfCertHttpsServer(callback); } }); } else { self._generateSelfCertHttpsServer(callback); } }); } } else { httpServer = http.createServer(app); callback(false, httpServer); } }; /** * Uses PEM module to generate a self-signed SSL certificate and creates * an https server from it, returning it in the callback. * @param callback * @private */ NodeApiServer.prototype._generateSelfCertHttpsServer = function (callback) { var ssl, httpServer; // Generate our own self-signed cert for easy use of https prototyping pem.createCertificate({ days: 1, selfSigned: true }, function(err, keys) { if (!err) { // Write self-signed cert data so we can re-use it fs.writeFile('./forerunnerdbTempCert/key.pem', keys.serviceKey, function () {}); fs.writeFile('./forerunnerdbTempCert/cert.pem', keys.certificate, function () {}); // Assign data to the ssl object ssl = { key: keys.serviceKey, cert: keys.certificate }; httpServer = https.createServer(ssl, app); callback(false, httpServer); } else { throw('Cannot enable SSL, generating a self-signed certificate failed: ' + err); } }); }; /** * Stops the server listener. */ NodeApiServer.prototype.stop = function () { if (server) { server.close(); server = undefined; this.emit('stopped'); return true; } return false; }; /** * Handles requests from clients to the endpoints the database exposes. * @param req * @param res */ NodeApiServer.prototype.handleRequest = function (req, res) { var self = this, method = req.method, dbName = req.params.dbName, objType = req.params.objType, objName = req.params.objName, objId = req.params.objId, query, options, db, obj, urlPath, pathSections; if (self.debug &amp;&amp; self.debug()) { console.log(self.logIdentifier() + ' Received REST: ' + method + ' ' + req.url); } // Check permissions self.hasPermission(dbName, objType, objName, method, req, function (err, results) { if (!err) { if (self.debug &amp;&amp; self.debug()) { console.log(self.logIdentifier() + ' REST call is allowed: ' + method + ' ' + req.url); } // Check if the database has this type of object // TODO: Do we want to call collectionExists (objType + 'Exists') here? if (typeof self._core.db(dbName)[objType] === 'function') { // Get the query and option params query = req.json &amp;&amp; req.json.$query ? req.json.$query : undefined; options = req.json &amp;&amp; req.json.$options ? req.json.$options : undefined; db = self._core.db(dbName); obj = db[objType](objName); // Check the object type for special considerations if (objType === 'procedure') { if (obj &amp;&amp; obj.exec &amp;&amp; typeof obj.exec === 'function') { // Handle procedure if (self.debug &amp;&amp; self.debug()) { console.log(self.logIdentifier() + ' REST executing procedure: ' + method + ' ' + req.url); } return obj.exec(req, res); } else { if (self.debug &amp;&amp; self.debug()) { console.log(self.logIdentifier() + ' REST procedure not found: ' + method + ' ' + req.url); } return res.status(404).send('Procedure "' + objName + '" not found!'); } } // Get url path urlPath = url.parse(req.url).pathname; // Split path into sections pathSections = urlPath.split('/'); if (self._rootPath) { // Remove the first 5 sections as we already used these (rootPath, dbName, objType, objName, itemId) pathSections.splice(0, 4 + self._rootSectionCount); } else { // Remove the first 4 sections as we already used these (dbName, objType, objName, itemId) pathSections.splice(0, 4); } switch (method) { case 'HEAD': case 'GET': if (obj.isProcessingQueue &amp;&amp; obj.isProcessingQueue()) { if (db.debug()) { console.log(db.logIdentifier() + ' Waiting for async queue: ' + objName); } obj.once('ready', function () { if (db.debug()) { console.log(db.logIdentifier() + ' Async queue complete: ' + objName); } self._handleResponse(req, res, dbName, objType, objName, obj, pathSections, obj.find(query, options)); }); } else { self._handleResponse(req, res, dbName, objType, objName, obj, pathSections, obj.find(query, options)); } break; case 'POST': if (obj.isProcessingQueue &amp;&amp; obj.isProcessingQueue()) { if (db.debug()) { console.log(db.logIdentifier() + ' Waiting for async queue: ' + objName); } obj.once('ready', function () { if (db.debug()) { console.log(db.logIdentifier() + ' Async queue complete: ' + objName); } obj.insert(req.body, function (result) { res.send(result); }); }); } else { obj.insert(req.body, function (result) { res.send(result); }); } break; case 'PUT': if (obj.isProcessingQueue &amp;&amp; obj.isProcessingQueue()) { if (db.debug()) { console.log(db.logIdentifier() + ' Waiting for async queue: ' + objName); } obj.once('ready', function () { if (db.debug()) { console.log(db.logIdentifier() + ' Async queue complete: ' + objName); } res.send(obj.updateById(objId, {$replace: req.body})); }); } else { res.send(obj.updateById(objId, {$replace: req.body})); } break; case 'PATCH': if (obj.isProcessingQueue &amp;&amp; obj.isProcessingQueue()) { if (db.debug()) { console.log(db.logIdentifier() + ' Waiting for async queue: ' + objName); } obj.once('ready', function () { if (db.debug()) { console.log(db.logIdentifier() + ' Async queue complete: ' + objName); } res.send(obj.updateById(objId, req.body)); }); } else { res.send(obj.updateById(objId, req.body)); } break; case 'DELETE': if (!objId) { // Remove all if (obj.isProcessingQueue &amp;&amp; obj.isProcessingQueue()) { if (db.debug()) { console.log(db.logIdentifier() + ' Waiting for async queue: ' + objName); } obj.once('ready', function () { if (db.debug()) { console.log(db.logIdentifier() + ' Async queue complete: ' + objName); } res.send(obj.remove(query, options)); }); } else { res.send(obj.remove(query, options)); } } else { // Remove one if (obj.isProcessingQueue &amp;&amp; obj.isProcessingQueue()) { if (db.debug()) { console.log(db.logIdentifier() + ' Waiting for async queue: ' + objName); } obj.once('ready', function () { if (db.debug()) { console.log(db.logIdentifier() + ' Async queue complete: ' + objName); } res.send(obj.removeById(objId, options)); }); } else { res.send(obj.removeById(objId, options)); } } break; default: res.status(403).send('Unknown method'); break; } if (self.debug &amp;&amp; self.debug()) { console.log(self.logIdentifier() + ' REST call finished: ' + method + ' ' + req.url); } } else { if (self.debug &amp;&amp; self.debug()) { console.log(self.logIdentifier() + ' REST call object type unknown: ' + method + ' ' + req.url); } res.status(500).send('Unknown object type: ' + objType); } } else { if (self.debug &amp;&amp; self.debug()) { console.log(self.logIdentifier() + ' REST call permission denied: ' + method + ' ' + req.url); } res.status(403).send(err); } }); }; NodeApiServer.prototype._handleResponse = function (req, res, dbName, objType, objName, obj, pathSections, responseData) { var self = this, pathSection, tmpParts, pathIdField = obj._primaryKey !== undefined ? obj._primaryKey : '_id', i; // Check if we need to generate a sub-document query if (pathSections.length) { // Get the next path section pathSection = pathSections.shift(); if (responseData instanceof Array) { // Set the id field to default pathIdField = '_id'; // Check if the patch section has a colon in it, denoting a search field and value if (pathSection.indexOf(':') > -1) { // Split the path section tmpParts = pathSection.split(':'); pathIdField = tmpParts[0]; pathSection = tmpParts[1]; } // The path section must correspond to an id in the array of items for (i = 0; i &lt; responseData.length; i++) { if (responseData[i][pathIdField] === pathSection) { return self._handleResponse(req, res, dbName, objType, objName, obj, pathSections, responseData[i]); } } // Could not find a matching document, send nothing back return self._sendResponse(req, res); } // Grab the document sub-data return self._handleResponse(req, res, dbName, objType, objName, obj, pathSections, responseData[pathSection]); } else { return self._sendResponse(req, res, responseData); } }; NodeApiServer.prototype._sendResponse = function (req, res, data) { var statusCode = data !== undefined ? 200 : 404; switch (req.method) { case 'HEAD': // Only send status codes - head is a unique case in that a // blank data string means we should send 204 (no content) // not 200, and if undefined we can still send 404 if (data !== undefined) { res.sendStatus(204); } else { res.sendStatus(404); } break; case 'GET': // Send status code and data res.status(statusCode).send(data); break; default: res.send(data); break; } }; /** * Handles client requests to open an EventSource connection to our * server-sent events server. * @param req * @param res */ NodeApiServer.prototype.handleSyncRequest = function (req, res) { var self = this, dbName = req.params.dbName, objType = req.params.objType, objName = req.params.objName, db, obj; // Check permissions self.hasPermission(dbName, objType, objName, "SYNC", req, function (err, results) { if (!err) { // Check if the database has this type of object // TODO: Do we want to call collectionExists (objType + 'Exists') here? if (typeof self._core.db(dbName)[objType] === 'function') { db = self._core.db(dbName); obj = db[objType](objName); // Let request last as long as possible req.socket.setTimeout(0x7FFFFFFF); // Ensure we have basic IO objects set up _io[objType] = _io[objType] || {}; _io[objType][objName] = _io[objType][objName] || { messageId: 0, clients: [] }; // Add this resource object the io clients array _io[objType][objName].clients.push({req: req, res: res}); // Check if we already have an io for this object if (!_io[objType][objName].io) { // Setup a chain reactor IO node to intercept CRUD packets // coming from the object (collection, view etc), and then // pass them to the clients _io[objType][objName].io = new ReactorIO(obj, self, function (chainPacket) { self.sendToAll(_io[objType][objName], chainPacket.type, chainPacket.data); // Returning false informs the chain reactor to continue propagation // of the chain packet down the graph tree return false; }); } req.socket.setNoDelay(true); // Send headers for event-stream connection res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); res.write(':ok\n\n'); // Send connected message to the client with messageId zero self.sendToClient(res, 0, 'connected', "{}"); req.on("close", function() { // Remove this client from the array var index = _io[objType][objName].clients.indexOf(res); if (index > -1) { _io[objType][objName].clients.splice(index, 1); } }); } else { res.status(500).send('Unknown object type: ' + objType); } } else { res.status(403).send(err); } }); }; /** * Sends server-sent-events message to all connected clients that are listening * to the changes in the IO that is passed. * @param io * @param eventName * @param data */ NodeApiServer.prototype.sendToAll = function (io, eventName, data) { var self = this, clientArr, cleanData, res, i; // Increment the message counter io.messageId++; clientArr = io.clients; cleanData = self.jStringify(data); // Loop client resource and write data out to socket for (i = 0; i &lt; clientArr.length; i++) { res = clientArr[i].res; self.sendToClient(res, io.messageId, eventName, cleanData); } }; /** * Sends data to individual client. * @param res * @param messageId * @param eventName * @param stringifiedData Data to send in already-stringified format. */ NodeApiServer.prototype.sendToClient = function (res, messageId, eventName, stringifiedData) { res.write('event: ' + eventName + '\n'); res.write('id: ' + messageId + '\n'); res.write("data: " + stringifiedData + '\n\n'); //console.log('EventStream: "' + eventName + '" ' + stringifiedData); }; /** * Checks for permission to access the specified object. * @param {String} dbName * @param {String} objType * @param {String} objName * @param {String} methodName * @param {Object} req * @param {Function} callback * @returns {*} */ NodeApiServer.prototype.hasPermission = function (dbName, objType, objName, methodName, req, callback) { var permissionMethods = this._core.api.access(dbName, objType, objName, methodName); if (!permissionMethods || !permissionMethods.length) { // No permissions set, deny access by default return callback('403 Access Forbidden'); } // Add a method right at the beginning of the waterfall // that passes in the req object so all the access control // methods can analyse the request for info they might need permissionMethods.splice(0, 0, function (cb) { cb(null, dbName, objType, objName, methodName, req); }); // Loop the access methods and call each one in turn until an error // response is found, then callback a failure async.waterfall(permissionMethods, callback); }; /** * Defines an access rule for an object and HTTP method combination. When * access is requested via a REST call, the function provided will be * executed and the callback from that method will determine if the * access will be allowed or denied. Multiple access functions can * be provided for a single model and method allowing authentication * checks to be stacked. * * This call also allows you to pass a wildcard "*" instead of the objType, * objName and methodName parameters which will match all items. This * allows you to set permissions globally. * * If no access permissions are present ForerunnerDB will automatically * deny any request to the resource by default. * @name access * @method NodeApiServer.access * @param {String} dbName The name of the database to set access rules for. * @param {String} objType The type of object that the name refers to * e.g. "collection", "view" etc. This effectively maps to a method name on * the Db class instance. If you can do db.collection() then you can use the * string "collection" since it maps to the db.collection() method. This means * you can also use this to map to custom classes as long as you register * an accessor method on the Db class. * @param {String} objName The object name to apply the access rule to. * @param {String} methodName The name of the HTTP method to apply the access * function to e.g. "GET", "POST", "PUT", "PATCH" etc. * @param {Function|String} checkFunction The function to call when an access * attempt is made against the collection. A callback method is passed to this * function which should be called after the function has finished * processing. If you do not need custom logic to allow or deny access you * can simply pass the string "allow" or "deny" instead of a function and * ForerunnerDB will handle the logic automatically. * @returns {*} */ NodeApiServer.prototype.access = function (dbName, objType, objName, methodName, checkFunction) { var methodArr, accessOrderItem, foundEntry = false, i; if (objType !== undefined &amp;&amp; objName !== undefined &amp;&amp; methodName !== undefined) { if (checkFunction !== undefined) { if (checkFunction === 'allow') { // Provide default allow method checkFunction = this._allowMethod; } else if (checkFunction === 'deny') { // Provide default deny method checkFunction = this._denyMethod; } if (typeof checkFunction !== 'function') { throw('Cannot create an access rule with a checkFunction argument of a type other than function!'); } // Set new access permission with callback "checkFunction" _access.db = _access.db || {}; _access.db[dbName] = _access.db[dbName] || {}; _access.db[dbName][objType] = _access.db[dbName][objType] || {}; _access.db[dbName][objType][objName] = _access.db[dbName][objType][objName] || {}; _access.db[dbName][objType][objName][methodName] = true; _accessOrder.push({ dbName: dbName, objType: objType, objName: objName, methodName: methodName, checkFunction: checkFunction }); return this; } methodArr = []; // Do quick lookup check if (_access.db &amp;&amp; _access.db[dbName] &amp;&amp; _access.db[dbName][objType] &amp;&amp; _access.db[dbName][objType][objName] &amp;&amp; _access.db[dbName][objType][objName][methodName]) { foundEntry = true; } else if (_access.db &amp;&amp; _access.db[dbName] &amp;&amp; _access.db[dbName][objType] &amp;&amp; _access.db[dbName][objType][objName] &amp;&amp; _access.db[dbName][objType][objName]['*']) { foundEntry = true; } else if (_access.db &amp;&amp; _access.db[dbName] &amp;&amp; _access.db[dbName][objType] &amp;&amp; _access.db[dbName][objType]['*'] &amp;&amp; _access.db[dbName][objType]['*'][methodName]) { foundEntry = true; } else if (_access.db &amp;&amp; _access.db[dbName] &amp;&amp; _access.db[dbName][objType] &amp;&amp; _access.db[dbName][objType]['*'] &amp;&amp; _access.db[dbName][objType]['*']['*']) { foundEntry = true; } else if (_access.db &amp;&amp; _access.db[dbName] &amp;&amp; _access.db[dbName]['*'] &amp;&amp; _access.db[dbName]['*']['*'] &amp;&amp; _access.db[dbName]['*']['*']['*']) { foundEntry = true; } else { // None of the matching patterns exist, exit without permissions return methodArr; } // Now we know that at least one of the permission rules matches the passed pattern // so we loop through the permissions in order they are registered and add matching // ones to the methodArr array. for (i = 0; i &lt; _accessOrder.length; i++) { accessOrderItem = _accessOrder[i]; // Determine if the access data fits the query if (accessOrderItem.dbName === '*' || accessOrderItem.dbName === dbName) { if (accessOrderItem.objType === '*' || accessOrderItem.objType === objType) { if (accessOrderItem.objName === '*' || accessOrderItem.objName === objName) { if (accessOrderItem.methodName === '*' || accessOrderItem.methodName === methodName) { methodArr.push(accessOrderItem.checkFunction); } } } } } return methodArr; } return []; }; NodeApiServer.prototype._allowMethod = function defaultAllowMethod (dbName, objType, objName, httpMethod, req, callback) { callback(false, dbName, objType, objName, httpMethod, req); }; NodeApiServer.prototype._denyMethod = function defaultDenyMethod (dbName, objType, objName, httpMethod, req, callback) { callback('Access forbidden', dbName, objType, objName, httpMethod, req); }; /** * Creates the routes that express will expose to clients. * @private */ NodeApiServer.prototype._defineRoutes = function () { var self = this, root = this._rootPath; app.get(root + '/', function (req, res) { res.send({ server: 'ForerunnerDB', version: self._core.version() }); }); // Handle sync routes app.get(root + '/:dbName/:objType/:objName/_sync', function () { self.handleSyncRequest.apply(self, arguments); }); // Handle all other routes app.get(root + '/:dbName/:objType/:objName', function () { self.handleRequest.apply(self, arguments); }); app.get(root + '/:dbName/:objType/:objName/:objId', function () { self.handleRequest.apply(self, arguments); }); app.get(root + '/:dbName/:objType/:objName/:objId/*', function () { self.handleRequest.apply(self, arguments); }); app.head(root + '/:dbName/:objType/:objName/:objId', function () { self.handleRequest.apply(self, arguments); }); app.post(root + '/:dbName/:objType/:objName', function () { self.handleRequest.apply(self, arguments); }); app.put(root + '/:dbName/:objType/:objName/:objId', function () { self.handleRequest.apply(self, arguments); }); app.patch(root + '/:dbName/:objType/:objName/:objId', function () { self.handleRequest.apply(self, arguments); }); app.delete(root + '/:dbName/:objType/:objName', function () { self.handleRequest.apply(self, arguments); }); app.delete(root + '/:dbName/:objType/:objName/:objId', function () { self.handleRequest.apply(self, arguments); }); app.get(root + '/:dbName/:objType/:objName/_sync', function () { self.handleRequest.apply(self, arguments); }); app.get(root + '/:dbName/:objType/:objName/:objId/_sync', function () { self.handleRequest.apply(self, arguments); }); }; /* NodeApiServer.prototype._generateCert = function () { pem.createCertificate({ days: 365, selfSigned: true }, function(err, keys) { }); }; */ /** * Override the Core init to instantiate the plugin. * @returns {*} */ Core.prototype.init = function () { this.api = new NodeApiServer(this); return CoreInit.apply(this, arguments); }; Shared.finishModule('NodeApiServer'); module.exports = NodeApiServer;</code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="ActiveBucket.html">ActiveBucket</a></li><li><a href="Angular.html">Angular</a></li><li><a href="AutoBind.html">AutoBind</a></li><li><a href="Collection.html">Collection</a></li><li><a href="CollectionGroup.html">CollectionGroup</a></li><li><a href="Condition.html">Condition</a></li><li><a href="Core.html">Core</a></li><li><a href="Db.html">Db</a></li><li><a href="Document.html">Document</a></li><li><a href="Grid.html">Grid</a></li><li><a href="Highchart.html">Highchart</a></li><li><a href="Index2d.html">Index2d</a></li><li><a href="IndexBinaryTree.html">IndexBinaryTree</a></li><li><a href="IndexHashMap.html">IndexHashMap</a></li><li><a href="Infinilist.html">Infinilist</a></li><li><a href="KeyValueStore.html">KeyValueStore</a></li><li><a href="Metrics.html">Metrics</a></li><li><a href="MyModule.html">MyModule</a></li><li><a href="NodeApiClient.html">NodeApiClient</a></li><li><a href="NodeApiServer.html">NodeApiServer</a></li><li><a href="NodeRAS.html">NodeRAS</a></li><li><a href="Odm.html">Odm</a></li><li><a href="OldView.html">OldView</a></li><li><a href="Operation.html">Operation</a></li><li><a href="Overload.html">Overload</a></li><li><a href="Overview.html">Overview</a></li><li><a href="Overview_init.html">init</a></li><li><a href="Path.html">Path</a></li><li><a href="Persist.html">Persist</a></li><li><a href="Procedure.html">Procedure</a></li><li><a href="ReactorIO.html">ReactorIO</a></li><li><a href="Section.html">Section</a></li><li><a href="Serialiser.html">Serialiser</a></li><li><a href="Shared.overload.html">overload</a></li><li><a href="View.html">View</a></li></ul><h3>Mixins</h3><ul><li><a href="ChainReactor.html">ChainReactor</a></li><li><a href="Common.html">Common</a></li><li><a href="Constants.html">Constants</a></li><li><a href="Events.html">Events</a></li><li><a href="Matching.html">Matching</a></li><li><a href="Shared.html">Shared</a></li><li><a href="Sorting.html">Sorting</a></li><li><a href="Tags.html">Tags</a></li><li><a href="Triggers.html">Triggers</a></li><li><a href="Updating.html">Updating</a></li></ul><h3><a href="global.html">Global</a></h3> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Thu Mar 01 2018 11:34:22 GMT+0000 (GMT) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>